← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/launchpad/sync-button-bug-747546 into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/sync-button-bug-747546 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #747546 in Launchpad itself: "Derived Distros: Add a button to sync all packages changed in parent but not changed in child"
  https://bugs.launchpad.net/launchpad/+bug/747546

For more details, see:
https://code.launchpad.net/~allenap/launchpad/sync-button-bug-747546/+merge/60348

This branch adds a button to the +localpackagediffs page that syncs
all updates into a derived series for packages that have not been
changed in the derived series.

It is the completion of jtv's work in lp:~jtv/launchpad/bug-747546.

This branch is quite long, but a lot of it is lint fixes.

-- 
https://code.launchpad.net/~allenap/launchpad/sync-button-bug-747546/+merge/60348
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/sync-button-bug-747546 into lp:launchpad.
=== modified file 'database/sampledata/current-dev.sql'
--- database/sampledata/current-dev.sql	2011-05-04 16:46:43 +0000
+++ database/sampledata/current-dev.sql	2011-05-09 09:35:49 +0000
@@ -4179,9 +4179,9 @@
 
 ALTER TABLE distroseriesdifference DISABLE TRIGGER ALL;
 
-INSERT INTO distroseriesdifference (id, derived_series, source_package_name, package_diff, status, difference_type, parent_package_diff, source_version, parent_source_version, base_version) VALUES (1, 14, 19, NULL, 2, 3, NULL, '1.0.10-4deribuntu1', '1.0.9a-4ubuntu1', '1.0.9a-4ubuntu1');
-INSERT INTO distroseriesdifference (id, derived_series, source_package_name, package_diff, status, difference_type, parent_package_diff, source_version, parent_source_version, base_version) VALUES (2, 14, 9, 1, 1, 3, 2, '2.0.8-4deribuntu1', '2.0.9-1ubuntu2', '2.0.7-1ubuntu1');
-INSERT INTO distroseriesdifference (id, derived_series, source_package_name, package_diff, status, difference_type, parent_package_diff, source_version, parent_source_version, base_version) VALUES (3, 14, 1, NULL, 2, 3, NULL, '4.1.1-1deribuntu1', '4.1.2-1ubuntu1', '4.1.1-1ubuntu1');
+INSERT INTO distroseriesdifference (id, derived_series, parent_series, source_package_name, package_diff, status, difference_type, parent_package_diff, source_version, parent_source_version, base_version) VALUES (1, 14, 1, 19, NULL, 2, 3, NULL, '1.0.10-4deribuntu1', '1.0.9a-4ubuntu1', '1.0.9a-4ubuntu1');
+INSERT INTO distroseriesdifference (id, derived_series, parent_series, source_package_name, package_diff, status, difference_type, parent_package_diff, source_version, parent_source_version, base_version) VALUES (2, 14, 1, 9, 1, 1, 3, 2, '2.0.8-4deribuntu1', '2.0.9-1ubuntu2', '2.0.7-1ubuntu1');
+INSERT INTO distroseriesdifference (id, derived_series, parent_series, source_package_name, package_diff, status, difference_type, parent_package_diff, source_version, parent_source_version, base_version) VALUES (3, 14, 1, 1, NULL, 2, 3, NULL, '4.1.1-1deribuntu1', '4.1.2-1ubuntu1', '4.1.1-1ubuntu1');
 
 
 ALTER TABLE distroseriesdifference ENABLE TRIGGER ALL;

=== modified file 'database/schema/comments.sql'
--- database/schema/comments.sql	2011-04-15 16:17:21 +0000
+++ database/schema/comments.sql	2011-05-09 09:35:49 +0000
@@ -569,6 +569,7 @@
 -- DistroSeriesDifference
 COMMENT ON TABLE DistroSeriesDifference IS 'A difference of versions for a package in a derived distroseries and its parent distroseries.';
 COMMENT ON COLUMN DistroSeriesDifference.derived_series IS 'The derived distroseries with the difference from its parent.';
+COMMENT ON COLUMN DistroSeriesDifference.parent_series IS 'The parent distroseries with the difference from its child.';
 COMMENT ON COLUMN DistroSeriesDifference.source_package_name IS 'The name of the source package which is different in the two series.';
 COMMENT ON COLUMN DistroSeriesDifference.package_diff IS 'The most recent package diff that was created for the base version to derived version.';
 COMMENT ON COLUMN DistroSeriesDifference.parent_package_diff IS 'The most recent package diff that was created for the base version to the parent version.';

=== added file 'database/schema/patch-2208-64-0.sql'
--- database/schema/patch-2208-64-0.sql	1970-01-01 00:00:00 +0000
+++ database/schema/patch-2208-64-0.sql	2011-05-09 09:35:49 +0000
@@ -0,0 +1,11 @@
+-- Copyright 2010 Canonical Ltd.  This software is licensed under the
+-- GNU Affero General Public License version 3 (see the file LICENSE).
+SET client_min_messages=ERROR;
+
+ALTER TABLE DistroSeriesDifference
+    ADD COLUMN parent_series INTEGER NOT NULL
+        CONSTRAINT distroseriesdifference__parentseries__fk REFERENCES distroseries;
+
+CREATE INDEX distroseriesdifference__parent_series__idx ON DistroSeriesDifference(parent_series);
+
+INSERT INTO LaunchpadDatabaseRevision VALUES (2208, 64, 0);

=== added file 'database/schema/patch-2208-65-1.sql'
--- database/schema/patch-2208-65-1.sql	1970-01-01 00:00:00 +0000
+++ database/schema/patch-2208-65-1.sql	2011-05-09 09:35:49 +0000
@@ -0,0 +1,7 @@
+SET client_min_messages = ERROR;
+
+create index bug__new_patches__idx on bug(id)
+where latest_patch_uploaded IS NOT NULL AND duplicateof IS NULL;
+
+INSERT INTO LaunchpadDatabaseRevision VALUES (2208, 65, 1);
+

=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2011-05-04 17:14:18 +0000
+++ database/schema/security.cfg	2011-05-09 09:35:49 +0000
@@ -798,6 +798,7 @@
 public.distribution                           = SELECT
 public.distributionjob                        = SELECT, INSERT
 public.distributionsourcepackage              = SELECT, INSERT
+public.distroseriesparent                     = SELECT
 public.packagediff                            = SELECT, INSERT, UPDATE
 public.sourcepackagepublishinghistory         = SELECT, INSERT, UPDATE, DELETE
 type=user
@@ -1024,6 +1025,7 @@
 public.distributionjob                          = SELECT
 public.distroseries                             = SELECT
 public.distroseriesdifference                   = SELECT, INSERT, UPDATE
+public.distroseriesparent                       = SELECT
 public.job                                      = SELECT, UPDATE
 public.libraryfilealias                         = SELECT
 public.libraryfilecontent                       = SELECT

=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-04-12 13:43:36 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-05-09 09:35:49 +0000
@@ -112,6 +112,10 @@
     IHWSubmissionDevice,
     IHWVendorID,
     )
+from lp.registry.enum import (
+    DistroSeriesDifferenceStatus,
+    DistroSeriesDifferenceType,
+    )
 from lp.registry.interfaces.commercialsubscription import (
     ICommercialSubscription,
     )
@@ -486,6 +490,16 @@
     IDistroSeries, 'deriveDistroSeries', 'distribution', IDistribution)
 patch_collection_return_type(
     IDistroSeries, 'getDerivedSeries', IDistroSeries)
+patch_plain_parameter_type(
+    IDistroSeries, 'getDifferencesTo', 'parent_series', IDistroSeries)
+patch_choice_parameter_type(
+    IDistroSeries, 'getDifferencesTo', 'status', DistroSeriesDifferenceStatus)
+patch_choice_parameter_type(
+    IDistroSeries, 'getDifferencesTo', 'difference_type',
+    DistroSeriesDifferenceType)
+patch_collection_return_type(
+    IDistroSeries, 'getDifferencesTo', IDistroSeriesDifference)
+
 
 # IDistroSeriesDifference
 patch_reference_property(

=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2011-05-05 20:19:07 +0000
+++ lib/lp/registry/browser/distroseries.py	2011-05-09 09:35:49 +0000
@@ -95,6 +95,7 @@
 from lp.registry.interfaces.distroseriesdifference import (
     IDistroSeriesDifferenceSource,
     )
+from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.series import SeriesStatus
 from lp.services.features import getFeatureFlag
@@ -103,11 +104,20 @@
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.soyuz.browser.archive import PackageCopyingMixin
 from lp.soyuz.browser.packagesearch import PackageSearchViewBase
+from lp.soyuz.interfaces.distributionjob import IPackageCopyJobSource
 from lp.soyuz.interfaces.queue import IPackageUploadSet
+from lp.soyuz.model.queue import PackageUploadQueue
 from lp.translations.browser.distroseries import (
     check_distroseries_translations_viewable,
     )
 
+# DistroSeries statuses that benefit from mass package upgrade support.
+UPGRADABLE_SERIES_STATUSES = [
+    SeriesStatus.FUTURE,
+    SeriesStatus.EXPERIMENTAL,
+    SeriesStatus.DEVELOPMENT,
+    ]
+
 
 class DistroSeriesNavigation(GetitemNavigation, BugTargetTraversalMixin,
     StructuralSubscriptionTargetTraversalMixin):
@@ -434,6 +444,15 @@
         return self._num_differences(
             DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES)
 
+    @cachedproperty
+    def parent_series(self):
+        # XXX StevenK: This will currently throw NotOneError up the
+        # callstack if the child has more than one parent. This is okay
+        # *for now*, so we can get away with no UI changes.
+        dsp = getUtility(IDistroSeriesParentSet).getByDerivedSeries(
+            self.context).one()
+        return dsp.parent_series
+
 
 class DistroSeriesEditView(LaunchpadEditFormView, SeriesStatusMixin):
     """View class that lets you edit a DistroSeries object.
@@ -734,7 +753,7 @@
         return form.Fields(Choice(
             __name__='package_type',
             vocabulary=make_package_type_vocabulary(
-                self.context.parent_series.displayname,
+                self.parent_series.displayname,
                 self.search_higher_parent_option),
             default=DEFAULT_PACKAGE_TYPE,
             required=True))
@@ -838,30 +857,28 @@
     def cached_differences(self):
         """Return a batch navigator of filtered results."""
         if self.specified_package_type == NON_BLACKLISTED:
-            status=(
+            status = (
                 DistroSeriesDifferenceStatus.NEEDS_ATTENTION,)
             child_version_higher = False
         elif self.specified_package_type == BLACKLISTED:
-            status=(
+            status = (
                 DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
             child_version_higher = False
         elif self.specified_package_type == HIGHER_VERSION_THAN_PARENT:
-            status=(
+            status = (
                 DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
             child_version_higher = True
         elif self.specified_package_type == RESOLVED:
-            status=DistroSeriesDifferenceStatus.RESOLVED
+            status = DistroSeriesDifferenceStatus.RESOLVED
             child_version_higher = False
         else:
             raise AssertionError('specified_package_type unknown')
 
         differences = getUtility(
             IDistroSeriesDifferenceSource).getForDistroSeries(
-                self.context,
-                difference_type = self.differences_type,
+                self.context, difference_type=self.differences_type,
                 source_package_name_filter=self.specified_name_filter,
-                status=status,
-                child_version_higher=child_version_higher)
+                status=status, child_version_higher=child_version_higher)
         return BatchNavigator(differences, self.request)
 
     @cachedproperty
@@ -879,12 +896,21 @@
             differences = getUtility(
                 IDistroSeriesDifferenceSource).getForDistroSeries(
                     self.context,
-                    difference_type = self.differences_type,
+                    difference_type=self.differences_type,
                     status=(
                         DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
                         DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT))
             return not differences.is_empty()
 
+    @cachedproperty
+    def parent_series(self):
+        # XXX StevenK: This will currently throw NotOneError up the
+        # callstack if the child has more than one parent. This is okay
+        # *for now*, so we can get away with no UI changes.
+        dsp = getUtility(IDistroSeriesParentSet).getByDerivedSeries(
+            self.context).one()
+        return dsp.parent_series
+
 
 class DistroSeriesLocalDifferencesView(DistroSeriesDifferenceBaseView,
                                        LaunchpadFormView):
@@ -900,7 +926,7 @@
         # Update the label for sync action.
         self.initialize_sync_label(
             "Sync Selected %s Versions into %s" % (
-                self.context.parent_series.displayname,
+                self.parent_series.displayname,
                 self.context.displayname,
                 ))
         super(DistroSeriesLocalDifferencesView, self).initialize()
@@ -916,8 +942,8 @@
             'target="help">Read more about syncing from the parent series'
             '</a>).',
             self.context.displayname,
-            self.context.parent_series.fullseriesname,
-            self.context.parent_series.displayname)
+            self.parent_series.fullseriesname,
+            self.parent_series.displayname)
 
     @property
     def label(self):
@@ -925,7 +951,7 @@
             "Source package differences between '%s' and "
             "parent series '%s'" % (
                 self.context.displayname,
-                self.context.parent_series.displayname,
+                self.parent_series.displayname,
                 ))
 
     @action(_("Update"), name="update")
@@ -938,6 +964,56 @@
     def sync_sources(self, action, data):
         self._sync_sources(action, data)
 
+    def getUpgrades(self):
+        """Find straightforward package upgrades.
+
+        These are updates for packages that this distroseries shares
+        with a parent series, for which there have been updates in the
+        parent, and which do not have any changes in this series that
+        might complicate a sync.
+
+        :return: A result set of `DistroSeriesDifference`s.
+        """
+        return getUtility(IDistroSeriesDifferenceSource).getSimpleUpgrades(
+            self.context)
+
+    @action(_("Upgrade Packages"), name="upgrade", condition='canUpgrade')
+    def upgrade(self, action, data):
+        """Request synchronization of straightforward package upgrades."""
+        self.requestUpgrades()
+
+    def requestUpgrades(self):
+        """Request sync of packages that can be easily upgraded."""
+        target_distroseries = self.context
+        target_archive = target_distroseries.main_archive
+        differences_by_archive = (
+            getUtility(IDistroSeriesDifferenceSource)
+                .collateDifferencesByParentArchive(self.getUpgrades()))
+        for source_archive, differences in differences_by_archive.iteritems():
+            source_package_info = [
+                (difference.source_package_name.name,
+                 difference.parent_source_version)
+                for difference in differences]
+            getUtility(IPackageCopyJobSource).create(
+                source_package_info, source_archive, target_archive,
+                target_distroseries, PackagePublishingPocket.UPDATES)
+        self.request.response.addInfoNotification(
+            (u"Upgrades of {context.displayname} packages have been "
+             u"requested. Please give Launchpad some time to complete "
+             u"these.").format(context=self.context))
+
+    def canUpgrade(self, action=None):
+        """Should the form offer a packages upgrade?"""
+        if self.context.status not in UPGRADABLE_SERIES_STATUSES:
+            # A feature freeze precludes blanket updates.
+            return False
+        elif self.getUpgrades().is_empty():
+            # There are no simple updates to perform.
+            return False
+        else:
+            queue = PackageUploadQueue(self.context, None)
+            return check_permission("launchpad.Edit", queue)
+
 
 class DistroSeriesMissingPackagesView(DistroSeriesDifferenceBaseView,
                                       LaunchpadFormView):
@@ -964,7 +1040,7 @@
             "Packages that are listed here are those that have been added to "
             "the specific packages %s that were used to create %s. They are "
             "listed here so you can consider including them in %s.",
-            self.context.parent_series.displayname,
+            self.parent_series.displayname,
             self.context.displayname,
             self.context.displayname)
 
@@ -972,7 +1048,7 @@
     def label(self):
         return (
             "Packages in parent series '%s' but not in '%s'" % (
-                self.context.parent_series.displayname,
+                self.parent_series.displayname,
                 self.context.displayname,
                 ))
 
@@ -1007,14 +1083,14 @@
             "Packages that are listed here are those that have been added to "
             "%s but are not yet part of the parent series %s.",
             self.context.displayname,
-            self.context.parent_series.displayname)
+            self.parent_series.displayname)
 
     @property
     def label(self):
         return (
             "Packages in '%s' but not in parent series '%s'" % (
                 self.context.displayname,
-                self.context.parent_series.displayname,
+                self.parent_series.displayname,
                 ))
 
     @action(_("Update"), name="update")

=== modified file 'lib/lp/registry/browser/distroseriesdifference.py'
--- lib/lp/registry/browser/distroseriesdifference.py	2011-04-18 14:23:22 +0000
+++ lib/lp/registry/browser/distroseriesdifference.py	2011-05-09 09:35:49 +0000
@@ -45,6 +45,7 @@
     IDistroSeriesDifferenceComment,
     IDistroSeriesDifferenceCommentSource,
     )
+from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet
 from lp.registry.model.distroseriesdifferencecomment import (
     DistroSeriesDifferenceComment,
     )

=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
--- lib/lp/registry/browser/tests/test_distroseries.py	2011-05-07 16:03:41 +0000
+++ lib/lp/registry/browser/tests/test_distroseries.py	2011-05-09 09:35:49 +0000
@@ -20,6 +20,7 @@
 from testtools.content_type import UTF8_TEXT
 from testtools.matchers import (
     EndsWith,
+    Equals,
     LessThan,
     Not,
     )
@@ -32,6 +33,7 @@
 from canonical.launchpad.testing.pages import find_tag_by_id
 from canonical.launchpad.webapp.authorization import check_permission
 from canonical.launchpad.webapp.batching import BatchNavigator
+from canonical.launchpad.webapp.interfaces import BrowserNotificationLevel
 from canonical.launchpad.webapp.publisher import canonical_url
 from canonical.testing.layers import (
     DatabaseFunctionalLayer,
@@ -49,6 +51,7 @@
     DistroSeriesDifferenceType,
     )
 from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.series import SeriesStatus
 from lp.services.features import (
     get_relevant_feature_controller,
     getFeatureFlag,
@@ -64,12 +67,14 @@
 from lp.soyuz.interfaces.component import IComponentSet
 from lp.soyuz.interfaces.distributionjob import (
     IInitialiseDistroSeriesJobSource,
+    IPackageCopyJobSource,
     )
 from lp.soyuz.interfaces.sourcepackageformat import (
     ISourcePackageFormatSelectionSet,
     )
 from lp.soyuz.model.archivepermission import ArchivePermission
 from lp.testing import (
+    anonymous_logged_in,
     celebrity_logged_in,
     feature_flags,
     login_person,
@@ -77,6 +82,7 @@
     set_feature_flag,
     StormStatementRecorder,
     TestCaseWithFactory,
+    with_celebrity_logged_in,
     )
 from lp.testing.matchers import HasQueryCount
 from lp.testing.views import create_initialized_view
@@ -101,11 +107,11 @@
 
     def _createDifferenceAndGetView(self, difference_type):
         # Helper function to create a valid DSD.
-        distroseries = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
         self.factory.makeDistroSeriesDifference(
-            derived_series=distroseries, difference_type=difference_type)
-        return create_initialized_view(distroseries, '+index')
+            derived_series=dsp.derived_series,
+            difference_type=difference_type)
+        return create_initialized_view(dsp.derived_series, '+index')
 
     def test_num_differences(self):
         diff_type = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
@@ -131,9 +137,10 @@
     def _setupDifferences(self, name, parent_name, nb_diff_versions,
                           nb_diff_child, nb_diff_parent):
         # Helper to create DSD of the different types.
-        derived_series = self.factory.makeDistroSeries(
-            name=name,
-            parent_series=self.factory.makeDistroSeries(name=parent_name))
+        derived_series = self.factory.makeDistroSeries(name=name)
+        parent_series = self.factory.makeDistroSeries(name=parent_name)
+        self.factory.makeDistroSeriesParent(
+            derived_series=derived_series, parent_series=parent_series)
         self.simple_user = self.factory.makePerson()
         for i in range(nb_diff_versions):
             diff_type = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
@@ -404,6 +411,13 @@
 
         self.assertThat(html_content, parent_packagesets)
 
+    def _create_child_and_parent(self):
+        derived_series = self.factory.makeDistroSeries(name='derilucid')
+        parent_series = self.factory.makeDistroSeries(name='lucid')
+        self.factory.makeDistroSeriesParent(
+            derived_series=derived_series, parent_series=parent_series)
+        return (derived_series, parent_series)
+
 
 class TestDistroSeriesLocalDifferences(
     DistroSeriesDifferenceMixin, TestCaseWithFactory):
@@ -421,9 +435,7 @@
         # Test that the page includes the filter form if differences
         # are present
         login_person(self.simple_user)
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         self.factory.makeDistroSeriesDifference(
             derived_series=derived_series)
 
@@ -439,9 +451,7 @@
         # Test that the page doesn't includes the filter form if no
         # differences are present
         login_person(self.simple_user)
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
 
         view = create_initialized_view(
             derived_series, '+localpackagediffs', principal=self.simple_user)
@@ -457,7 +467,7 @@
         with celebrity_logged_in('admin'):
             ps = self.factory.makePackageset(
                 packages=[ds_diff.source_package_name],
-                distroseries=ds_diff.derived_series.parent_series)
+                distroseries=ds_diff.parent_series)
 
         with person_logged_in(self.simple_user):
             view = create_initialized_view(
@@ -480,27 +490,28 @@
                 self.factory.makePackageset(
                     name=name,
                     packages=[ds_diff.source_package_name],
-                    distroseries=ds_diff.derived_series.parent_series)
+                    distroseries=ds_diff.parent_series)
 
         with person_logged_in(self.simple_user):
             view = create_initialized_view(
                 ds_diff.derived_series,
                 '+localpackagediffs',
                 principal=self.simple_user)
-            html = view()
+            html_content = view()
 
         packageset_text = re.compile(
             '\s*' + ', '.join(sorted(unsorted_names)))
         self._test_packagesets(
-            html, packageset_text, 'parent-packagesets', 'Parent packagesets')
+            html_content, packageset_text, 'parent-packagesets',
+            'Parent packagesets')
 
     def test_queries(self):
         # With no DistroSeriesDifferences the query count should be low and
         # fairly static. However, with some DistroSeriesDifferences the query
         # count will be higher, but it should remain the same no matter how
         # many differences there are.
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        derived_series = dsp.derived_series
         ArchivePermission(
             archive=derived_series.main_archive, person=self.simple_user,
             component=getUtility(IComponentSet)["main"],
@@ -612,19 +623,46 @@
 class TestDistroSeriesLocalDifferencesZopeless(TestCaseWithFactory):
     """Test the distroseries +localpackagediffs view."""
 
-    layer = LaunchpadZopelessLayer
+    layer = LaunchpadFunctionalLayer
+
+    def makePackageUpgrade(self, derived_series=None):
+        """Create a `DistroSeriesDifference` for a package upgrade."""
+        base_version = '1.%d' % self.factory.getUniqueInteger()
+        versions = {
+            'base': base_version,
+            'parent': base_version + '-' + self.factory.getUniqueString(),
+            'derived': base_version,
+        }
+        return self.factory.makeDistroSeriesDifference(
+            derived_series=derived_series, versions=versions,
+            set_base_version=True)
+
+    def makeDerivedSeries(self):
+        """Create a derived `DistroSeries`."""
+        return self.factory.makeDistroSeries(
+            parent_series=self.factory.makeDistroSeries())
+
+    def makeView(self, distroseries=None):
+        """Create a +localpackagediffs view for `distroseries`."""
+        if distroseries is None:
+            distroseries = self.makeDerivedSeries()
+        return create_initialized_view(distroseries, '+localpackagediffs')
+
+    def _create_child_and_parent(self):
+        parent_series = self.factory.makeDistroSeries(name='lucid')
+        derived_series = self.factory.makeDistroSeries(name='derilucid')
+        self.factory.makeDistroSeriesParent(
+            derived_series=derived_series, parent_series=parent_series)
+        return (derived_series, parent_series)
 
     def test_view_redirects_without_feature_flag(self):
         # If the feature flag soyuz.derived-series-ui.enabled is not set the
         # view simply redirects to the derived series.
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
 
         self.assertIs(
             None, getFeatureFlag('soyuz.derived-series-ui.enabled'))
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         response = view.request.response
         self.assertEqual(302, response.getStatus())
@@ -633,12 +671,9 @@
 
     def test_label(self):
         # The view label includes the names of both series.
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
 
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         self.assertEqual(
             "Source package differences between 'Derilucid' and "
@@ -648,26 +683,21 @@
     def test_batch_includes_needing_attention_only(self):
         # The differences attribute includes differences needing
         # attention only.
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         current_difference = self.factory.makeDistroSeriesDifference(
             derived_series=derived_series)
         self.factory.makeDistroSeriesDifference(
             derived_series=derived_series,
             status=DistroSeriesDifferenceStatus.RESOLVED)
 
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         self.assertContentEqual(
             [current_difference], view.cached_differences.batch)
 
     def test_batch_includes_different_versions_only(self):
         # The view contains differences of type DIFFERENT_VERSIONS only.
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         different_versions_diff = self.factory.makeDistroSeriesDifference(
             derived_series=derived_series)
         self.factory.makeDistroSeriesDifference(
@@ -675,21 +705,16 @@
             difference_type=(
                 DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES))
 
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         self.assertContentEqual(
             [different_versions_diff], view.cached_differences.batch)
 
     def test_template_includes_help_link(self):
         # The help link for popup help is included.
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
-
+        derived_series, parent_series = self._create_child_and_parent()
         set_derived_series_ui_feature_flag(self)
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         soup = BeautifulSoup(view())
         help_links = soup.findAll(
@@ -698,17 +723,15 @@
 
     def test_diff_row_includes_last_comment_only(self):
         # The most recent comment is rendered for each difference.
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         difference = self.factory.makeDistroSeriesDifference(
             derived_series=derived_series)
-        difference.addComment(difference.owner, "Earlier comment")
-        difference.addComment(difference.owner, "Latest comment")
+        with person_logged_in(derived_series.owner):
+            difference.addComment(difference.owner, "Earlier comment")
+            difference.addComment(difference.owner, "Latest comment")
 
         set_derived_series_ui_feature_flag(self)
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
 
         # Find all the rows within the body of the table
         # listing the differences.
@@ -722,15 +745,12 @@
 
     def test_diff_row_links_to_extra_details(self):
         # The source package name links to the difference details.
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         difference = self.factory.makeDistroSeriesDifference(
             derived_series=derived_series)
 
         set_derived_series_ui_feature_flag(self)
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
         soup = BeautifulSoup(view())
         diff_table = soup.find('table', {'class': 'listing'})
         row = diff_table.tbody.findAll('tr')[0]
@@ -744,9 +764,7 @@
         # The +localpackagediffs page shows the version attached to the
         # DSD and not the last published version (bug=745776).
         package_name = 'package-1'
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         versions = {
             'base': u'1.0',
             'derived': u'1.0derived1',
@@ -768,8 +786,7 @@
             version=new_version)
 
         set_derived_series_ui_feature_flag(self)
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
         soup = BeautifulSoup(view())
         diff_table = soup.find('table', {'class': 'listing'})
         row = diff_table.tbody.tr
@@ -791,9 +808,7 @@
         # The +localpackagediffs page shows only the version (no link)
         # if we fail to fetch the published version.
         package_name = 'package-1'
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         versions = {
             'base': u'1.0',
             'derived': u'1.0derived1',
@@ -806,14 +821,16 @@
             derived_series=derived_series)
 
         # Delete the publications.
-        difference.source_pub.status = PackagePublishingStatus.DELETED
-        difference.parent_source_pub.status = PackagePublishingStatus.DELETED
+        with celebrity_logged_in("admin"):
+            difference.source_pub.status = (
+                PackagePublishingStatus.DELETED)
+            difference.parent_source_pub.status = (
+                PackagePublishingStatus.DELETED)
         # Flush out the changes and invalidate caches (esp. property caches).
         flush_database_caches()
 
         set_derived_series_ui_feature_flag(self)
-        view = create_initialized_view(
-            derived_series, '+localpackagediffs')
+        view = self.makeView(derived_series)
         soup = BeautifulSoup(view())
         diff_table = soup.find('table', {'class': 'listing'})
         row = diff_table.tbody.tr
@@ -830,17 +847,126 @@
         self.assertEqual(versions['derived'], derived_span[0].string.strip())
         self.assertEqual(versions['parent'], parent_span[0].string.strip())
 
+    def test_getUpgrades_shows_updates_in_parent(self):
+        # The view's getUpgrades methods lists packages that can be
+        # trivially upgraded: changed in the parent, not changed in the
+        # derived series, but present in both.
+        dsd = self.makePackageUpgrade()
+        view = self.makeView(dsd.derived_series)
+        self.assertContentEqual([dsd], view.getUpgrades())
+
+    def test_upgrades_are_offered_if_appropriate(self):
+        # The"Upgrade Packages" button will only be shown to privileged users.
+        dsd = self.makePackageUpgrade()
+        view = self.makeView(dsd.derived_series)
+        with celebrity_logged_in("admin"):
+            self.assertTrue(view.canUpgrade())
+        with person_logged_in(self.factory.makePerson()):
+            self.assertFalse(view.canUpgrade())
+        with anonymous_logged_in():
+            self.assertFalse(view.canUpgrade())
+
+    @with_celebrity_logged_in("admin")
+    def test_upgrades_offered_only_if_available(self):
+        # If there are no upgrades, the "Upgrade Packages" button won't
+        # be shown.
+        view = self.makeView()
+        self.assertFalse(view.canUpgrade())
+        self.makePackageUpgrade()
+        self.assertTrue(view.canUpgrade())
+
+    @with_celebrity_logged_in("admin")
+    def test_upgrades_not_offered_after_feature_freeze(self):
+        # There won't be an "Upgrade Packages" button once feature
+        # freeze has occurred.  Mass updates would not make sense after
+        # that point.
+        upgradeable = {}
+        for status in SeriesStatus.items:
+            dsd = self.makePackageUpgrade()
+            dsd.derived_series.status = status
+            view = self.makeView(dsd.derived_series)
+            upgradeable[status] = view.canUpgrade()
+        expected = {
+            SeriesStatus.FUTURE: True,
+            SeriesStatus.EXPERIMENTAL: True,
+            SeriesStatus.DEVELOPMENT: True,
+            SeriesStatus.FROZEN: False,
+            SeriesStatus.CURRENT: False,
+            SeriesStatus.SUPPORTED: False,
+            SeriesStatus.OBSOLETE: False,
+        }
+        self.assertEqual(expected, upgradeable)
+
+    def test_upgrade_creates_sync_jobs(self):
+        # requestUpgrades generates PackageCopyJobs for the upgrades
+        # that need doing.
+        dsd = self.makePackageUpgrade()
+        series = dsd.derived_series
+        view = self.makeView(series)
+        view.requestUpgrades()
+        job_source = getUtility(IPackageCopyJobSource)
+        jobs = list(
+            job_source.getActiveJobs(series.distribution.main_archive))
+        self.assertEquals(1, len(jobs))
+        job = jobs[0]
+        self.assertEquals(series, job.distroseries)
+        source_package_info = list(job.source_packages)
+        self.assertEquals(1, len(source_package_info))
+        self.assertEqual(
+            (dsd.source_package_name.name, dsd.parent_source_version),
+            source_package_info[0][:2])
+
+    def test_upgrade_gives_feedback(self):
+        # requestUpgrades doesn't instantly perform package upgrades,
+        # but it shows the user a notice that the upgrades have been
+        # requested.
+        dsd = self.makePackageUpgrade()
+        view = self.makeView(dsd.derived_series)
+        view.requestUpgrades()
+        expected = {
+            "level": BrowserNotificationLevel.INFO,
+            "message":
+                ("Upgrades of {0.displayname} packages have been "
+                 "requested. Please give Launchpad some time to "
+                 "complete these.").format(dsd.derived_series),
+            }
+        observed = map(vars, view.request.response.notifications)
+        self.assertEqual([expected], observed)
+
+    def test_requestUpgrade_is_efficient(self):
+        # A single web request may need to schedule large numbers of
+        # package upgrades.  It must do so without issuing large numbers
+        # of database queries.
+        derived_series, parent_series = self._create_child_and_parent()
+        # Take a baseline measure of queries.
+        self.makePackageUpgrade(derived_series=derived_series)
+        flush_database_caches()
+        with StormStatementRecorder() as recorder1:
+            self.makeView(derived_series).requestUpgrades()
+        self.assertThat(recorder1, HasQueryCount(LessThan(10)))
+        # The query count does not increase with more differences.
+        for index in xrange(3):
+            self.makePackageUpgrade(derived_series=derived_series)
+        flush_database_caches()
+        with StormStatementRecorder() as recorder2:
+            self.makeView(derived_series).requestUpgrades()
+        self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
+
 
 class TestDistroSeriesLocalDifferencesFunctional(TestCaseWithFactory):
 
     layer = LaunchpadFunctionalLayer
 
+    def _create_child_and_parent(self):
+        parent_series = self.factory.makeDistroSeries(name='lucid')
+        derived_series = self.factory.makeDistroSeries(name='derilucid')
+        self.factory.makeDistroSeriesParent(
+            derived_series=derived_series, parent_series=parent_series)
+        return (derived_series, parent_series)
+
     def test_higher_radio_mentions_parent(self):
         set_derived_series_ui_feature_flag(self)
-        parent_series = self.factory.makeDistroSeries(
-            name='lucid', displayname='Lucid')
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=parent_series)
+        derived_series, parent_series = self._create_child_and_parent()
         self.factory.makeDistroSeriesDifference(
             derived_series=derived_series,
             source_package_name_str="my-src-package")
@@ -867,9 +993,7 @@
     def test_batch_filtered(self):
         # The name_filter parameter allows filtering of packages by name.
         set_derived_series_ui_feature_flag(self)
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         diff1 = self.factory.makeDistroSeriesDifference(
             derived_series=derived_series,
             source_package_name_str="my-src-package")
@@ -893,9 +1017,7 @@
     def test_batch_non_blacklisted(self):
         # The default filter is all non blacklisted differences.
         set_derived_series_ui_feature_flag(self)
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         diff1 = self.factory.makeDistroSeriesDifference(
             derived_series=derived_series,
             source_package_name_str="my-src-package")
@@ -923,9 +1045,7 @@
         # field.package_type parameter allows to list only
         # blacklisted differences.
         set_derived_series_ui_feature_flag(self)
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         blacklisted_diff = self.factory.makeDistroSeriesDifference(
             derived_series=derived_series,
             status=DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
@@ -947,9 +1067,7 @@
         # field.package_type parameter allows to list only
         # blacklisted differences with a child's version higher than parent's.
         set_derived_series_ui_feature_flag(self)
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
         blacklisted_diff_higher = self.factory.makeDistroSeriesDifference(
             derived_series=derived_series,
             status=DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
@@ -977,9 +1095,7 @@
         # Test that we can search for differences that we marked
         # resolved.
         set_derived_series_ui_feature_flag(self)
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
 
         self.factory.makeDistroSeriesDifference(
             derived_series=derived_series,
@@ -1008,7 +1124,9 @@
             distribution = self.factory.makeDistribution('deribuntu')
         derived_series = self.factory.makeDistroSeries(
             distribution=distribution,
-            name='derilucid', parent_series=parent_series)
+            name='derilucid')
+        self.factory.makeDistroSeriesParent(
+            derived_series=derived_series, parent_series=parent_series)
         self._set_source_selection(derived_series)
         self.factory.makeDistroSeriesDifference(
             source_package_name_str=src_name,
@@ -1271,8 +1389,8 @@
     def test_missingpackages_differences(self):
         # The view fetches the differences with type
         # MISSING_FROM_DERIVED_SERIES.
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        derived_series = dsp.derived_series
 
         missing_type = DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES
         # Missing blacklisted diff.
@@ -1295,8 +1413,8 @@
     def test_missingpackages_differences_empty(self):
         # The view is empty if there is no differences with type
         # MISSING_FROM_DERIVED_SERIES.
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        derived_series = dsp.derived_series
 
         not_missing_type = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
 
@@ -1334,18 +1452,19 @@
         with celebrity_logged_in('admin'):
             ps = self.factory.makePackageset(
                 packages=[self.ds_diff.source_package_name],
-                distroseries=self.ds_diff.derived_series.parent_series)
+                distroseries=self.ds_diff.parent_series)
 
         with person_logged_in(self.simple_user):
             view = create_initialized_view(
                 self.ds_diff.derived_series,
                 '+missingpackages',
                 principal=self.simple_user)
-            html = view()
+            html_content = view()
 
         packageset_text = re.compile('\s*' + ps.name)
         self._test_packagesets(
-            html, packageset_text, 'parent-packagesets', 'Parent packagesets')
+            html_content, packageset_text, 'parent-packagesets',
+            'Parent packagesets')
 
 
 class DistroSerieUniquePackageDiffsTestCase(TestCaseWithFactory):
@@ -1353,12 +1472,17 @@
 
     layer = LaunchpadZopelessLayer
 
+    def _create_child_and_parent(self):
+        derived_series = self.factory.makeDistroSeries(name='derilucid')
+        parent_series = self.factory.makeDistroSeries(name='lucid')
+        self.factory.makeDistroSeriesParent(
+            derived_series=derived_series, parent_series=parent_series)
+        return (derived_series, parent_series)
+
     def test_uniquepackages_differences(self):
         # The view fetches the differences with type
         # UNIQUE_TO_DERIVED_SERIES.
-        derived_series = self.factory.makeDistroSeries(
-            name='derilucid', parent_series=self.factory.makeDistroSeries(
-                name='lucid'))
+        derived_series, parent_series = self._create_child_and_parent()
 
         missing_type = DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES
         # Missing blacklisted diff.
@@ -1381,8 +1505,7 @@
     def test_uniquepackages_differences_empty(self):
         # The view is empty if there is no differences with type
         # UNIQUE_TO_DERIVED_SERIES.
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        derived_series, parent_series = self._create_child_and_parent()
 
         not_missing_type = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
 

=== added file 'lib/lp/registry/browser/tests/test_distroseries_webservice.py'
--- lib/lp/registry/browser/tests/test_distroseries_webservice.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_distroseries_webservice.py	2011-05-09 09:35:49 +0000
@@ -0,0 +1,44 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from canonical.testing import AppServerLayer
+from lp.registry.enum import (
+    DistroSeriesDifferenceStatus,
+    DistroSeriesDifferenceType,
+    )
+from lp.testing import (
+    TestCaseWithFactory,
+    ws_object,
+    )
+
+
+class DistroSeriesWebServiceTestCase(TestCaseWithFactory):
+
+    layer = AppServerLayer
+
+    def assertSameDiffs(self, diffs, ws_diffs):
+        self.assertContentEqual(
+            [self._wsFor(diff) for diff in diffs],
+            [ws_diff for ws_diff in ws_diffs])
+
+    def _wsFor(self, obj):
+        return ws_object(
+            self.factory.makeLaunchpadService(version="devel"), obj)
+
+    def test_getDifferencesTo(self):
+        # Distroseries' DistroSeriesDifferences are available
+        # on the web service.
+        # This method is a simple wrapper around getForDistroSeries
+        # that is thoroughly tested in test_distroseriesdifference.
+        ds_diff = self.factory.makeDistroSeriesDifference()
+        ds_ws = self._wsFor(ds_diff.derived_series)
+
+        self.assertSameDiffs([ds_diff], ds_ws.getDifferencesTo(
+            status=str(DistroSeriesDifferenceStatus.NEEDS_ATTENTION),
+            difference_type=str(
+                DistroSeriesDifferenceType.DIFFERENT_VERSIONS),
+            source_package_name_filter=ds_diff.source_package_name.name,
+            child_version_higher=False,
+            parent_series=self._wsFor(ds_diff.parent_series)))

=== modified file 'lib/lp/registry/errors.py'
--- lib/lp/registry/errors.py	2010-12-10 20:54:12 +0000
+++ lib/lp/registry/errors.py	2011-05-09 09:35:49 +0000
@@ -5,6 +5,7 @@
 __all__ = [
     'DistroSeriesDifferenceError',
     'NotADerivedSeriesError',
+    'MultipleParentsForDerivedSeriesError',
     'CannotTransitionToCountryMirror',
     'CountryMirrorAlreadySet',
     'DeleteSubscriptionError',
@@ -125,6 +126,14 @@
     non-derived series - that is, a distroseries with a null Parent."""
 
 
+class MultipleParentsForDerivedSeriesError(Exception):
+    """A distro series difference must have one parent series set.
+
+    This is raised when a DistroSeriesDifference is created, and we can't
+    determine the parent series to use, since there is more than one. The
+    parent series needs to be specified in this case."""
+
+
 class TeamMembershipTransitionError(ValueError):
     """Indicates something has gone wrong with the transtiion.
 

=== modified file 'lib/lp/registry/interfaces/distribution.py'
--- lib/lp/registry/interfaces/distribution.py	2011-04-14 20:40:03 +0000
+++ lib/lp/registry/interfaces/distribution.py	2011-05-09 09:35:49 +0000
@@ -156,7 +156,7 @@
         Summary(
             title=_("Summary"),
             description=_(
-                "A short paragraph to introduce the the goals and highlights "
+                "A short paragraph to introduce the goals and highlights "
                 "of the distribution."),
             required=True))
     homepage_content = exported(

=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py	2011-04-17 18:00:45 +0000
+++ lib/lp/registry/interfaces/distroseries.py	2011-05-09 09:35:49 +0000
@@ -24,6 +24,7 @@
     export_write_operation,
     exported,
     LAZR_WEBSERVICE_EXPORTED,
+    operation_for_version,
     operation_parameters,
     operation_returns_collection_of,
     operation_returns_entry,
@@ -896,6 +897,46 @@
     def getDerivedSeries():
         """Get all `DistroSeries` derived from this one."""
 
+    @operation_parameters(
+        parent_series=Reference(
+            schema=Interface, # IDistroSeries
+            title=_("The parent series to consider."),
+            required=False),
+        difference_type=Choice(
+            vocabulary=DBEnumeratedType, # DistroSeriesDifferenceType
+            title=_("Only return differences of this type."), required=False),
+        source_package_name_filter=TextLine(
+            title=_("Only return differences for packages matching this "
+                    "name."),
+            required=False),
+        status=Choice(
+            vocabulary=DBEnumeratedType, # DistroSeriesDifferenceStatus
+            title=_("Only return differences of this status."),
+            required=False),
+        child_version_higher=Bool(
+            title=_("Only return differences for which the child's version "
+                    "is higher than the parent's."),
+            required=False),
+        )
+    @operation_returns_collection_of(Interface)
+    @export_read_operation()
+    @operation_for_version('devel')
+    def getDifferencesTo(parent_series, difference_type,
+                         source_package_name_filter, status,
+                         child_version_higher):
+        """Return the differences between this series and the specified
+        parent_series (or all the parent series if parent_series is None).
+
+        :param parent_series: The parent series for which the differences
+            should be returned. All parents are considered if this is None.
+        :param difference_type: The type of the differences to return.
+        :param source_package_name_filter: A package name to use as a filter
+            for the differences.
+        :param status: The status of the differences to return.
+        :param child_version_higher: Only return differences for which the
+            child's version is higher than the parent's version.
+        """
+
 
 class IDistroSeries(IDistroSeriesEditRestricted, IDistroSeriesPublic,
                     IStructuralSubscriptionTarget):

=== modified file 'lib/lp/registry/interfaces/distroseriesdifference.py'
--- lib/lp/registry/interfaces/distroseriesdifference.py	2011-04-19 02:57:38 +0000
+++ lib/lp/registry/interfaces/distroseriesdifference.py	2011-05-09 09:35:49 +0000
@@ -55,8 +55,14 @@
     derived_series = exported(Reference(
         IDistroSeries, title=_("Derived series"), required=True,
         readonly=True, description=_(
-            "The distribution series which, together with its parent, "
-            "identifies the two series with the difference.")))
+            "The distribution series which identifies the derived series "
+            "with the difference.")))
+
+    parent_series = exported(Reference(
+        IDistroSeries, title=_("Parent series"), required=True,
+        readonly=True, description=_(
+            "The distribution series which identifies the parent series "
+            "with the difference.")))
 
     source_package_name = Reference(
         ISourcePackageName,
@@ -189,7 +195,7 @@
         """
 
     latest_comment = Reference(
-        Interface, # IDistroSeriesDifferenceComment
+        Interface,  # IDistroSeriesDifferenceComment
         title=_("The latest comment"),
         readonly=True)
 
@@ -253,7 +259,7 @@
 class IDistroSeriesDifferenceSource(Interface):
     """A utility of this interface can be used to create differences."""
 
-    def new(derived_series, source_package_name):
+    def new(derived_series, source_package_name, parent_series=None):
         """Create an `IDistroSeriesDifference`.
 
         :param derived_series: The distribution series which was derived
@@ -263,6 +269,10 @@
         :param source_package_name: A source package name identifying the
             package with a difference.
         :type source_package_name: `ISourcePackageName`.
+        :param parent_series: The distribution series which has the derived
+            series as a child. If there is only one parent, it does not need
+            to be specified.
+        :type parent_series: `IDistroSeries`.
         :raises NotADerivedSeriesError: When the passed distro series
             is not a derived series.
         :return: A new `DistroSeriesDifference` object.
@@ -270,10 +280,11 @@
 
     def getForDistroSeries(
         distro_series,
-        difference_type=DistroSeriesDifferenceType.DIFFERENT_VERSIONS,
+        difference_type=None,
         source_package_name_filter=None,
         status=None,
-        child_version_higher=False):
+        child_version_higher=False,
+        parent_series=None):
         """Return differences for the derived distro series sorted by
         package name.
 
@@ -291,7 +302,10 @@
         :param child_version_higher: Only differences for which the child's
             version is higher than the parent's version will be included.
         :type child_version_higher: bool.
-        :return: A result set of differences.
+        :param parent_series: The parent series to consider. Consider all
+            parent series if this parameter is None.
+        :type distro_series: `IDistroSeries`.
+        :return: A result set of `IDistroSeriesDifference`.
         """
 
     def getByDistroSeriesAndName(distro_series, source_package_name):
@@ -303,3 +317,24 @@
         :param source_package_name: The name of the package difference.
         :type source_package_name: unicode.
         """
+
+    def getSimpleUpgrades(distro_series):
+        """Find pending upgrades that can be performed mindlessly.
+
+        These are `DistroSeriesDifferences` where the parent has been
+        updated and the child still has the old version, unchanged.
+
+        Blacklisted items are excluded.
+        """
+
+    def collateDifferencesByParentArchive(differences):
+        """Collate the given differences by parent archive.
+
+        The given `IDistroSeriesDifference`s are returned in a `dict`, with
+        the parent `Archive` as keys.
+
+        :param differences: An iterable sequence of `IDistroSeriesDifference`.
+
+        :return: A `dict` of iterable sequences of `IDistroSeriesDifference`
+            keyed by their parent `IArchive`.
+        """

=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py	2011-05-05 15:44:17 +0000
+++ lib/lp/registry/model/distroseries.py	2011-05-09 09:35:49 +0000
@@ -99,6 +99,9 @@
     IDistroSeries,
     IDistroSeriesSet,
     )
+from lp.registry.interfaces.distroseriesdifference import (
+    IDistroSeriesDifferenceSource,
+    )
 from lp.registry.interfaces.person import validate_public_person
 from lp.registry.interfaces.pocket import (
     PackagePublishingPocket,
@@ -788,9 +791,12 @@
     @property
     def is_derived_series(self):
         """See `IDistroSeries`."""
-        # XXX rvb 2011-04-11 bug=754750: This should be cleaned up once
-        # the bug is fixed.
-        return self.parent_series is not None
+        # Circular imports.
+        from lp.registry.interfaces.distroseriesparent import (
+            IDistroSeriesParentSet,
+            )
+        dsps = getUtility(IDistroSeriesParentSet).getByDerivedSeries(self)
+        return not dsps.is_empty()
 
     @property
     def is_initialising(self):
@@ -2002,17 +2008,12 @@
 
     def getDerivedSeries(self):
         """See `IDistroSeriesPublic`."""
-        # XXX rvb 2011-04-08 bug=754750: The clause
-        # 'DistroSeries.distributionID!=self.distributionID' is only
-        # required because the parent_series attribute has been
-        # (mis-)used to denote other relations than proper derivation
-        # relashionships. We should be rid of this condition once
-        # the bug is fixed.
-        results = Store.of(self).find(
-            DistroSeries,
-            DistroSeries.parent_series==self.id,
-            DistroSeries.distributionID!=self.distributionID)
-        return results.order_by(Desc(DistroSeries.date_created))
+        # Circular imports.
+        from lp.registry.interfaces.distroseriesparent import (
+            IDistroSeriesParentSet,
+            )
+        dsps = getUtility(IDistroSeriesParentSet).getByParentSeries(self)
+        return [dsp.derived_series for dsp in dsps]
 
     def getBugTaskWeightFunction(self):
         """Provide a weight function to determine optimal bug task.
@@ -2034,6 +2035,18 @@
                 return OrderedBugTask(3, bugtask.id, bugtask)
         return weight_function
 
+    def getDifferencesTo(self, parent_series=None, difference_type=None,
+                         source_package_name_filter=None, status=None,
+                         child_version_higher=False):
+        """See `IDistroSeries`."""
+        return getUtility(
+            IDistroSeriesDifferenceSource).getForDistroSeries(
+                self,
+                difference_type = difference_type,
+                source_package_name_filter=source_package_name_filter,
+                status=status,
+                child_version_higher=child_version_higher)
+
 
 class DistroSeriesSet:
     implements(IDistroSeriesSet)

=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
--- lib/lp/registry/model/distroseriesdifference.py	2011-05-03 07:10:37 +0000
+++ lib/lp/registry/model/distroseriesdifference.py	2011-05-09 09:35:49 +0000
@@ -18,13 +18,13 @@
     )
 from lazr.enum import DBItem
 from sqlobject import StringCol
+from storm.exceptions import NotOneError
 from storm.expr import (
     And,
     compile as storm_compile,
     Desc,
     SQL,
     )
-from storm.info import ClassAlias
 from storm.locals import (
     Int,
     Reference,
@@ -52,6 +52,7 @@
     )
 from lp.registry.errors import (
     DistroSeriesDifferenceError,
+    MultipleParentsForDerivedSeriesError,
     NotADerivedSeriesError,
     )
 from lp.registry.interfaces.distroseriesdifference import (
@@ -61,6 +62,7 @@
 from lp.registry.interfaces.distroseriesdifferencecomment import (
     IDistroSeriesDifferenceCommentSource,
     )
+from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.model.distroseries import DistroSeries
 from lp.registry.model.distroseriesdifferencecomment import (
@@ -110,7 +112,6 @@
         )
     conditions = And(
         DistroSeriesDifference.id.is_in(dsd.id for dsd in dsds),
-        DistroSeries.id == DistroSeriesDifference.derived_series_id,
         SourcePackagePublishingHistory.archiveID == Archive.id,
         SourcePackagePublishingHistory.sourcepackagereleaseID == (
             SourcePackageRelease.id),
@@ -120,16 +121,16 @@
         )
     # Check in the parent archive or the child?
     if in_parent:
-        ParentDistroSeries = ClassAlias(DistroSeries)
         conditions = And(
             conditions,
-            ParentDistroSeries.id == DistroSeries.parent_seriesID,
-            Archive.distributionID == ParentDistroSeries.distributionID,
+            DistroSeries.id == DistroSeriesDifference.parent_series_id,
+            Archive.distributionID == DistroSeries.distributionID,
             Archive.purpose == ArchivePurpose.PRIMARY,
             )
     else:
         conditions = And(
             conditions,
+            DistroSeries.id == DistroSeriesDifference.derived_series_id,
             Archive.distributionID == DistroSeries.distributionID,
             Archive.purpose == ArchivePurpose.PRIMARY,
             )
@@ -197,6 +198,9 @@
     derived_series = Reference(
         derived_series_id, 'DistroSeries.id')
 
+    parent_series_id = Int(name='parent_series', allow_none=False)
+    parent_series = Reference(parent_series_id, 'DistroSeries.id')
+
     source_package_name_id = Int(
         name='source_package_name', allow_none=False)
     source_package_name = Reference(
@@ -222,14 +226,25 @@
     base_version = StringCol(dbName='base_version', notNull=False)
 
     @staticmethod
-    def new(derived_series, source_package_name):
+    def new(derived_series, source_package_name, parent_series=None):
         """See `IDistroSeriesDifferenceSource`."""
-        if not derived_series.is_derived_series:
-            raise NotADerivedSeriesError()
+        if parent_series is None:
+            try:
+                dsps = getUtility(IDistroSeriesParentSet)
+                dsp = dsps.getByDerivedSeries(
+                    derived_series).one()
+            except NotOneError:
+                raise MultipleParentsForDerivedSeriesError()
+            else:
+                if dsp is None:
+                    raise NotADerivedSeriesError()
+                else:
+                    parent_series = dsp.parent_series
 
         store = IMasterStore(DistroSeriesDifference)
         diff = DistroSeriesDifference()
         diff.derived_series = derived_series
+        diff.parent_series = parent_series
         diff.source_package_name = source_package_name
 
         # The status and type is set to default values - they will be
@@ -243,11 +258,14 @@
     @staticmethod
     def getForDistroSeries(
         distro_series,
-        difference_type=DistroSeriesDifferenceType.DIFFERENT_VERSIONS,
+        difference_type=None,
         source_package_name_filter=None,
         status=None,
-        child_version_higher=False):
+        child_version_higher=False,
+        parent_series=None):
         """See `IDistroSeriesDifferenceSource`."""
+        if difference_type is None:
+            difference_type = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
         if status is None:
             status = (
                 DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
@@ -261,7 +279,11 @@
             DistroSeriesDifference.status.is_in(status),
             DistroSeriesDifference.source_package_name ==
                 SourcePackageName.id,
-         ]
+        ]
+
+        if parent_series:
+            conditions.extend([
+               DistroSeriesDifference.parent_series == parent_series.id])
 
         if source_package_name_filter:
             conditions.extend([
@@ -330,8 +352,7 @@
                     spph = parent_source_pubs_for_release[spn_id]
                     cache.parent_source_package_release = (
                         DistroSeriesSourcePackageRelease(
-                            dsd.derived_series.parent_series,
-                            spph.sourcepackagerelease))
+                            dsd.parent_series, spph.sourcepackagerelease))
                 else:
                     cache.parent_source_package_release = None
                 cache.latest_comment = latest_comment_by_dsd_id.get(dsd.id)
@@ -373,6 +394,38 @@
                 SourcePackageName.id),
             SourcePackageName.name == source_package_name).one()
 
+    @staticmethod
+    def getSimpleUpgrades(distro_series):
+        """See `IDistroSeriesDifferenceSource`.
+
+        Eager-load related `ISourcePackageName` records.
+        """
+        differences = IStore(DistroSeriesDifference).find(
+            (DistroSeriesDifference, SourcePackageName),
+            DistroSeriesDifference.derived_series == distro_series,
+            DistroSeriesDifference.difference_type ==
+                DistroSeriesDifferenceType.DIFFERENT_VERSIONS,
+            DistroSeriesDifference.status ==
+                DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
+            DistroSeriesDifference.parent_source_version !=
+                DistroSeriesDifference.base_version,
+            DistroSeriesDifference.source_version ==
+                DistroSeriesDifference.base_version,
+            SourcePackageName.id ==
+                DistroSeriesDifference.source_package_name_id)
+        return DecoratedResultSet(differences, itemgetter(0))
+
+    @staticmethod
+    def collateDifferencesByParentArchive(differences):
+        by_archive = dict()
+        for difference in differences:
+            archive = difference.parent_series.main_archive
+            if archive in by_archive:
+                by_archive[archive].append(difference)
+            else:
+                by_archive[archive] = [difference]
+        return by_archive
+
     @cachedproperty
     def source_pub(self):
         """See `IDistroSeriesDifference`."""
@@ -387,7 +440,7 @@
         """Helper to keep source_pub/parent_source_pub DRY."""
         distro_series = self.derived_series
         if for_parent:
-            distro_series = self.derived_series.parent_series
+            distro_series = self.parent_series
 
         pubs = distro_series.getPublishedSources(
             self.source_package_name, include_pending=True)
@@ -402,7 +455,7 @@
     def base_source_pub(self):
         """See `IDistroSeriesDifference`."""
         if self.base_version is not None:
-            parent = self.derived_series.parent_series
+            parent = self.parent_series
             result = parent.main_archive.getPublishedSources(
                 name=self.source_package_name.name,
                 version=self.base_version).first()
@@ -424,7 +477,7 @@
     @property
     def title(self):
         """See `IDistroSeriesDifference`."""
-        parent_name = self.derived_series.parent_series.displayname
+        parent_name = self.parent_series.displayname
         return ("Difference between distroseries '%(parent_name)s' and "
                 "'%(derived_name)s' for package '%(pkg_name)s' "
                 "(%(parent_version)s/%(source_version)s)" % {
@@ -479,14 +532,8 @@
 
     def getParentPackageSets(self):
         """See `IDistroSeriesDifference`."""
-        has_parent_series = self.derived_series is not None and (
-            self.derived_series.parent_series is not None)
-        if has_parent_series:
-            return getUtility(IPackagesetSet).setsIncludingSource(
-                self.source_package_name,
-                self.derived_series.parent_series)
-        else:
-            return []
+        return getUtility(IPackagesetSet).setsIncludingSource(
+            self.source_package_name, self.parent_series)
 
     @property
     def package_diff_status(self):
@@ -507,14 +554,12 @@
     @cachedproperty
     def parent_source_package_release(self):
         return self._package_release(
-            self.derived_series.parent_series,
-            self.parent_source_version)
+            self.parent_series, self.parent_source_version)
 
     @cachedproperty
     def source_package_release(self):
         return self._package_release(
-            self.derived_series,
-            self.source_version)
+            self.derived_series, self.source_version)
 
     def _package_release(self, distro_series, version):
         statuses = (

=== modified file 'lib/lp/registry/scripts/populate_distroseriesdiff.py'
--- lib/lp/registry/scripts/populate_distroseriesdiff.py	2011-04-27 07:42:09 +0000
+++ lib/lp/registry/scripts/populate_distroseriesdiff.py	2011-05-09 09:35:49 +0000
@@ -16,11 +16,12 @@
     'PopulateDistroSeriesDiff',
     ]
 
+from collections import defaultdict
 from optparse import (
     Option,
     OptionValueError,
     )
-from storm.info import ClassAlias
+from storm.locals import ClassAlias
 import transaction
 from zope.component import getUtility
 
@@ -35,9 +36,11 @@
     )
 from canonical.launchpad.utilities.looptuner import TunableLoop
 from lp.registry.interfaces.distribution import IDistributionSet
+from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.model.distroseries import DistroSeries
 from lp.registry.model.distroseriesdifference import DistroSeriesDifference
+from lp.registry.model.distroseriesparent import DistroSeriesParent
 from lp.services.scripts.base import LaunchpadScript
 from lp.soyuz.interfaces.publishing import active_publishing_status
 
@@ -78,7 +81,7 @@
         """ % parameters
 
 
-def compose_sql_find_differences(derived_distroseries):
+def compose_sql_find_differences(derived_series, parent_series):
     """Produce SQL that finds differences for a `DistroSeries`.
 
     The query compares `derived_distroseries` and its `parent_series`
@@ -92,9 +95,9 @@
     """
     parameters = {
         'derived_query': compose_sql_find_latest_source_package_releases(
-            derived_distroseries),
+            derived_series),
         'parent_query': compose_sql_find_latest_source_package_releases(
-            derived_distroseries.parent_series),
+            parent_series),
     }
     return """
         SELECT DISTINCT
@@ -137,7 +140,8 @@
         """ % parameters
 
 
-def compose_sql_populate_distroseriesdiff(derived_distroseries, temp_table):
+def compose_sql_populate_distroseriesdiff(derived_series, parent_series,
+                                          temp_table):
     """Create `DistroSeriesDifference` rows based on found differences.
 
     Uses field values that describe the difference, as produced by the
@@ -154,7 +158,8 @@
     :return: SQL query, as a string.
     """
     parameters = {
-        'derived_series': quote(derived_distroseries),
+        'derived_series': quote(derived_series),
+        'parent_series': quote(parent_series),
         'difference_type_expression': compose_sql_difference_type(),
         'needs_attention': quote(
             DistroSeriesDifferenceStatus.NEEDS_ATTENTION),
@@ -163,6 +168,7 @@
     return """
         INSERT INTO DistroSeriesDifference (
             derived_series,
+            parent_series,
             source_package_name,
             status,
             difference_type,
@@ -170,6 +176,7 @@
             parent_source_version)
         SELECT
             %(derived_series)s,
+            %(parent_series)s,
             sourcepackagename,
             %(needs_attention)s,
             %(difference_type_expression)s,
@@ -188,7 +195,7 @@
     store.execute("DROP TABLE IF EXISTS %s" % quote_identifier(table))
 
 
-def populate_distroseriesdiff(logger, derived_distroseries):
+def populate_distroseriesdiff(logger, derived_series, parent_series):
     """Compare `derived_distroseries` to parent, and register differences.
 
     The differences are registered by creating `DistroSeriesDifference`
@@ -196,32 +203,33 @@
     """
     temp_table = "temp_potentialdistroseriesdiff"
 
-    store = IStore(derived_distroseries)
+    store = IStore(derived_series)
     drop_table(store, temp_table)
     store.execute("CREATE TEMP TABLE %s AS %s" % (
         quote_identifier(temp_table),
-        compose_sql_find_differences(derived_distroseries)))
+        compose_sql_find_differences(derived_series, parent_series)))
     logger.info(
         "Found %d potential difference(s).",
         store.execute("SELECT count(*) FROM %s" % temp_table).get_one()[0])
     store.execute(
         compose_sql_populate_distroseriesdiff(
-            derived_distroseries, temp_table))
+            derived_series, parent_series, temp_table))
     drop_table(store, temp_table)
 
 
 def find_derived_series():
     """Find all derived `DistroSeries`.
-
-    Derived `DistroSeries` are ones that have a `parent_series`, but
-    where the `parent_series` is not in the same distribution.
     """
+    Child = ClassAlias(DistroSeries, "Child")
     Parent = ClassAlias(DistroSeries, "Parent")
-    return IStore(DistroSeries).find(
-        DistroSeries,
-        Parent.id == DistroSeries.parent_seriesID,
-        Parent.distributionID != DistroSeries.distributionID).order_by(
-            (DistroSeries.parent_seriesID, DistroSeries.id))
+    relations = IStore(DistroSeries).find(
+        (Child, Parent),
+        DistroSeriesParent.derived_series_id == Child.id,
+        DistroSeriesParent.parent_series_id == Parent.id)
+    collated = defaultdict(list)
+    for child, parent in relations:
+        collated[child].append(parent)
+    return collated
 
 
 class DSDUpdater(TunableLoop):
@@ -301,25 +309,33 @@
     def getDistroSeries(self):
         """Return the `DistroSeries` that are to be processed."""
         if self.options.all:
-            return list(find_derived_series())
+            return find_derived_series()
         else:
             distro = getUtility(IDistributionSet).getByName(
                 self.options.distribution)
             series = distro.getSeries(self.options.series)
+            augmented_series = defaultdict(list)
             if series is None:
                 raise OptionValueError(
                     "Could not find %s series %s." % (
                         self.options.distribution, self.options.series))
-            if series.parent_series is None:
+            dsp = getUtility(IDistroSeriesParentSet).getByDerivedSeries(
+                series)
+            for rel in dsp:
+                augmented_series[rel.derived_series].append(
+                    rel.parent_series)
+            if len(augmented_series) == 0:
                 raise OptionValueError(
                     "%s series %s is not derived." % (
                         self.options.distribution, self.options.series))
-            return [series]
+            return augmented_series
 
-    def processDistroSeries(self, distroseries):
+    def processDistroSeries(self, distroseries, parent):
         """Generate `DistroSeriesDifference`s for `distroseries`."""
-        self.logger.info("Looking for differences in %s.", distroseries)
-        populate_distroseriesdiff(self.logger, distroseries)
+        self.logger.info(
+            "Looking for differences in %s with regards to %s.",
+            distroseries, parent)
+        populate_distroseriesdiff(self.logger, distroseries, parent)
         self.commit()
         self.logger.info("Updating base_versions.")
         self.update(distroseries)
@@ -335,8 +351,12 @@
 
     def listDerivedSeries(self):
         """Log all `DistroSeries` that the --all option would cover."""
-        for series in self.getDistroSeries():
-            self.logger.info("%s %s", series.distribution.name, series.name)
+        relationships = self.getDistroSeries()
+        for child in relationships:
+            for parent in relationships[child]:
+               self.logger.info(
+                    "%s %s with a parent of %s %s", child.distribution.name,
+                    child.name, parent.distribution.name, parent.name)
 
     def checkOptions(self):
         """Verify command-line options."""
@@ -363,8 +383,10 @@
         if self.options.dry_run:
             self.logger.info("Dry run requested.  Not committing changes.")
 
-        for series in self.getDistroSeries():
-            self.processDistroSeries(series)
+        relationships = self.getDistroSeries()
+        for child in relationships.keys():
+            for parent in relationships[child]:
+                self.processDistroSeries(child, parent)
 
     def update(self, distroseries):
         """Call `DistroSeriesDifference.update()` where appropriate.

=== modified file 'lib/lp/registry/scripts/tests/test_populate_distroseriesdiff.py'
--- lib/lp/registry/scripts/tests/test_populate_distroseriesdiff.py	2011-04-27 06:41:58 +0000
+++ lib/lp/registry/scripts/tests/test_populate_distroseriesdiff.py	2011-05-09 09:35:49 +0000
@@ -88,8 +88,7 @@
 
     def makeDerivedDistroSeries(self):
         """Create a `DistroSeries` that's derived from another distro."""
-        return self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        return self.factory.makeDistroSeriesParent()
 
     def getDistroSeriesDiff(self, distroseries):
         """Find the `DistroSeriesDifference` records for `distroseries`."""
@@ -218,35 +217,45 @@
     layer = ZopelessDatabaseLayer
 
     def test_baseline(self):
-        query = compose_sql_find_differences(self.makeDerivedDistroSeries())
+        dsp = self.makeDerivedDistroSeries()
+        query = compose_sql_find_differences(
+            dsp.derived_series, dsp.parent_series)
         self.assertIsInstance(query, basestring)
 
     def test_finds_nothing_for_empty_distroseries(self):
-        distroseries = self.makeDerivedDistroSeries()
-        query = compose_sql_find_differences(distroseries)
-        self.assertContentEqual([], Store.of(distroseries).execute(query))
+        dsp = self.makeDerivedDistroSeries()
+        query = compose_sql_find_differences(
+            dsp.derived_series, dsp.parent_series)
+        self.assertContentEqual(
+            [], Store.of(dsp.derived_series).execute(query))
 
     def test_does_not_find_grandparents_packages(self):
-        parent = self.makeDerivedDistroSeries()
-        distroseries = self.factory.makeDistroSeries(parent_series=parent)
-        self.makeSPPH(distroseries=parent.parent_series)
-        query = compose_sql_find_differences(distroseries)
-        self.assertContentEqual([], Store.of(distroseries).execute(query))
+        dsp = self.makeDerivedDistroSeries()
+        grandparent = self.factory.makeDistroSeriesParent(
+            derived_series=dsp.parent_series)
+        self.makeSPPH(distroseries=grandparent.parent_series)
+        query = compose_sql_find_differences(
+            dsp.derived_series, dsp.parent_series)
+        self.assertContentEqual(
+            [], Store.of(dsp.derived_series).execute(query))
 
     def test_does_not_find_identical_releases(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
         spr = self.factory.makeSourcePackageRelease()
         self.makeSPPH(
-            distroseries=distroseries.parent_series, sourcepackagerelease=spr)
+            distroseries=dsp.parent_series, sourcepackagerelease=spr)
         self.makeSPPH(
-            distroseries=distroseries, sourcepackagerelease=spr)
-        query = compose_sql_find_differences(distroseries)
-        self.assertContentEqual([], Store.of(distroseries).execute(query))
+            distroseries=dsp.derived_series, sourcepackagerelease=spr)
+        query = compose_sql_find_differences(
+            dsp.derived_series, dsp.parent_series)
+        self.assertContentEqual(
+            [], Store.of(dsp.derived_series).execute(query))
 
     def test_ignores_releases_for_same_version(self):
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         version_string = self.factory.getUniqueString()
-        parent_series = derived_series.parent_series
+        parent_series = dsp.parent_series
         package = self.factory.makeSourcePackageName()
         self.makeSPPH(
             distroseries=derived_series,
@@ -258,25 +267,27 @@
             sourcepackagerelease=self.factory.makeSourcePackageRelease(
                 sourcepackagename=package, distroseries=parent_series,
                 version=version_string))
-        query = compose_sql_find_differences(derived_series)
+        query = compose_sql_find_differences(derived_series, parent_series)
         self.assertContentEqual([], Store.of(derived_series).execute(query))
 
     def test_finds_release_missing_in_derived_series(self):
-        distroseries = self.makeDerivedDistroSeries()
-        spph = self.makeSPPH(distroseries=distroseries.parent_series)
-        query = compose_sql_find_differences(distroseries)
+        dsp = self.makeDerivedDistroSeries()
+        spph = self.makeSPPH(distroseries=dsp.parent_series)
+        query = compose_sql_find_differences(
+            dsp.derived_series, dsp.parent_series)
         self.assertContentEqual(
             [(
                 spph.sourcepackagerelease.sourcepackagenameID,
                 None,
                 spph.sourcepackagerelease.version,
             )],
-            Store.of(distroseries).execute(query))
+            Store.of(dsp.derived_series).execute(query))
 
     def test_finds_release_unique_to_derived_series(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        distroseries = dsp.derived_series
         spph = self.makeSPPH(distroseries=distroseries)
-        query = compose_sql_find_differences(distroseries)
+        query = compose_sql_find_differences(distroseries, dsp.parent_series)
         self.assertContentEqual(
             [(
                 spph.sourcepackagerelease.sourcepackagenameID,
@@ -286,10 +297,11 @@
             Store.of(distroseries).execute(query))
 
     def test_does_not_conflate_releases_of_different_packages(self):
-        distroseries = self.makeDerivedDistroSeries()
-        parent_spph = self.makeSPPH(distroseries=distroseries.parent_series)
+        dsp = self.makeDerivedDistroSeries()
+        distroseries = dsp.derived_series
+        parent_spph = self.makeSPPH(distroseries=dsp.parent_series)
         derived_spph = self.makeSPPH(distroseries=distroseries)
-        query = compose_sql_find_differences(distroseries)
+        query = compose_sql_find_differences(distroseries, dsp.parent_series)
         self.assertEqual(2, Store.of(distroseries).execute(query).rowcount)
         self.assertContentEqual([(
                 parent_spph.sourcepackagerelease.sourcepackagenameID,
@@ -303,8 +315,9 @@
             Store.of(distroseries).execute(query))
 
     def test_finds_different_releases_of_same_package(self):
-        distroseries = self.makeDerivedDistroSeries()
-        parent_series = distroseries.parent_series
+        dsp = self.makeDerivedDistroSeries()
+        distroseries = dsp.derived_series
+        parent_series = dsp.parent_series
         spn = self.factory.makeSourcePackageName()
         parent_spph = self.makeSPPH(
             distroseries=parent_series,
@@ -314,7 +327,7 @@
             distroseries=distroseries,
             sourcepackagerelease=self.factory.makeSourcePackageRelease(
                 distroseries=distroseries, sourcepackagename=spn))
-        query = compose_sql_find_differences(distroseries)
+        query = compose_sql_find_differences(distroseries, parent_series)
         self.assertContentEqual(
             [(
                 parent_spph.sourcepackagerelease.sourcepackagenameID,
@@ -324,8 +337,9 @@
             Store.of(distroseries).execute(query))
 
     def test_finds_newer_release_even_when_same_release_also_exists(self):
-        derived_series = self.makeDerivedDistroSeries()
-        parent_series = derived_series.parent_series
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
+        parent_series = dsp.parent_series
         spn = self.factory.makeSourcePackageName()
         shared_spr = self.factory.makeSourcePackageRelease(
             distroseries=parent_series, sourcepackagename=spn)
@@ -339,7 +353,7 @@
             distroseries=derived_series, sourcepackagename=spn)
         self.makeSPPH(
             distroseries=derived_series, sourcepackagerelease=newer_spr)
-        query = compose_sql_find_differences(derived_series)
+        query = compose_sql_find_differences(derived_series, parent_series)
         self.assertContentEqual(
             [(
                 parent_spph.sourcepackagerelease.sourcepackagenameID,
@@ -408,14 +422,8 @@
         self.assertNotIn(distroseries, find_derived_series())
 
     def test_finds_derived_distroseries(self):
-        self.assertIn(self.makeDerivedDistroSeries(), find_derived_series())
-
-    def test_ignores_parent_within_same_distro(self):
-        parent_series = self.factory.makeDistroSeries()
-        derived_series = self.factory.makeDistroSeries(
-            distribution=parent_series.distribution,
-            parent_series=parent_series)
-        self.assertNotIn(derived_series, find_derived_series())
+        dsp = self.makeDerivedDistroSeries()
+        self.assertIn(dsp.derived_series, find_derived_series())
 
 
 class TestPopulateDistroSeriesDiff(TestCaseWithFactory, FactoryHelper):
@@ -424,23 +432,26 @@
     layer = LaunchpadFunctionalLayer
 
     def test_baseline(self):
-        distroseries = self.factory.makeDistroSeries()
-        query = compose_sql_populate_distroseriesdiff(distroseries, "tmp")
+        dsp = self.factory.makeDistroSeriesParent()
+        query = compose_sql_populate_distroseriesdiff(
+            dsp.derived_series, dsp.parent_series, "tmp")
         self.assertIsInstance(query, basestring)
 
     def test_creates_distroseriesdifference(self):
-        distroseries = self.makeDerivedDistroSeries()
-        spph = self.makeSPPH(distroseries=distroseries)
-        populate_distroseriesdiff(DevNullLogger(), distroseries)
-        store = Store.of(distroseries)
-        dsd = self.getDistroSeriesDiff(distroseries).one()
+        dsp = self.makeDerivedDistroSeries()
+        spph = self.makeSPPH(distroseries=dsp.derived_series)
+        populate_distroseriesdiff(
+            DevNullLogger(), dsp.derived_series, dsp.parent_series)
+        store = Store.of(dsp.derived_series)
+        dsd = self.getDistroSeriesDiff(dsp.derived_series).one()
         spr = spph.sourcepackagerelease
         self.assertEqual(spr.sourcepackagename, dsd.source_package_name)
         self.assertEqual(
             DistroSeriesDifferenceStatus.NEEDS_ATTENTION, dsd.status)
 
     def test_does_not_overwrite_distroseriesdifference(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        distroseries = dsp.derived_series
         changelog = self.factory.makeChangelog(versions=['3.1', '3.141'])
         parent_changelog = self.factory.makeChangelog(
             versions=['3.1', '3.14'])
@@ -522,24 +533,28 @@
         return script
 
     def test_finds_distroseries(self):
-        spph = self.makeSPPH(distroseries=self.makeDerivedDistroSeries())
+        dsp = self.makeDerivedDistroSeries()
+        spph = self.makeSPPH(distroseries=dsp.derived_series)
         script = self.makeScript([
             '--distribution', spph.distroseries.distribution.name,
             '--series', spph.distroseries.name,
             ])
-        self.assertEqual([spph.distroseries], script.getDistroSeries())
+        self.assertEqual(
+            [spph.distroseries], script.getDistroSeries().keys())
 
     def test_finds_all_distroseries(self):
-        spphs = [
-            self.makeSPPH(self.makeDerivedDistroSeries())
-            for counter in xrange(2)]
+        spphs = []
+        for counter in xrange(2):
+            dsp = self.makeDerivedDistroSeries()
+            spphs.append(self.makeSPPH(dsp.derived_series))
         script = self.makeScript(['--all'])
         distroseries = script.getDistroSeries()
         for spph in spphs:
             self.assertIn(spph.distroseries, distroseries)
 
     def test_populates_for_distroseries(self):
-        spph = self.makeSPPH(distroseries=self.makeDerivedDistroSeries())
+        dsp = self.makeDerivedDistroSeries()
+        spph = self.makeSPPH(distroseries=dsp.derived_series)
         script = self.makeScript([
             '--distribution', spph.distroseries.distribution.name,
             '--series', spph.distroseries.name,
@@ -549,7 +564,8 @@
             0, self.getDistroSeriesDiff(spph.distroseries).count())
 
     def test_commits_changes(self):
-        spph = self.makeSPPH(distroseries=self.makeDerivedDistroSeries())
+        dsp = self.makeDerivedDistroSeries()
+        spph = self.makeSPPH(distroseries=dsp.derived_series)
         script = self.makeScript([
             '--distribution', spph.distroseries.distribution.name,
             '--series', spph.distroseries.name,
@@ -562,14 +578,16 @@
             0, self.getDistroSeriesDiff(spph.distroseries).count())
 
     def test_dry_run_goes_through_the_motions(self):
-        spph = self.makeSPPH(distroseries=self.makeDerivedDistroSeries())
+        dsp = self.makeDerivedDistroSeries()
+        spph = self.makeSPPH(distroseries=dsp.derived_series)
         script = self.makeScript(['--all', '--dry-run'])
         script.processDistroSeries = FakeMethod
         script.main()
         self.assertNotEqual(0, script.processDistroSeries.call_count)
 
     def test_dry_run_does_not_commit_changes(self):
-        spph = self.makeSPPH(distroseries=self.makeDerivedDistroSeries())
+        dsp = self.makeDerivedDistroSeries()
+        spph = self.makeSPPH(distroseries=dsp.derived_series)
         transaction.commit()
         script = self.makeScript([
             '--distribution', spph.distroseries.distribution.name,
@@ -581,7 +599,8 @@
             [], self.getDistroSeriesDiff(spph.distroseries))
 
     def test_list(self):
-        spph = self.makeSPPH(distroseries=self.makeDerivedDistroSeries())
+        dsp = self.makeDerivedDistroSeries()
+        spph = self.makeSPPH(distroseries=dsp.derived_series)
         script = self.makeScript(['--list'])
         script.logger = BufferLogger()
         script.main()
@@ -590,7 +609,8 @@
         self.assertIn(expected_series_name, script.logger.getLogBuffer())
 
     def test_calls_update(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        distroseries = dsp.derived_series
         self.makeSPPH(distroseries=distroseries)
         script = self.makeScript([
             '--distribution', distroseries.distribution.name,
@@ -602,7 +622,8 @@
             [((distroseries,), {})], script.update.calls)
 
     def test_fixes_base_versions(self):
-        distroseries = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        distroseries = dsp.derived_series
         package = self.factory.makeSourcePackageName()
         derived_spr = self.factory.makeSourcePackageRelease(
             distroseries=distroseries, sourcepackagename=package)
@@ -610,11 +631,9 @@
             distroseries=distroseries,
             sourcepackagerelease=derived_spr)
         parent_spr = self.factory.makeSourcePackageRelease(
-            distroseries=distroseries.parent_series,
-            sourcepackagename=package)
+            distroseries=dsp.parent_series, sourcepackagename=package)
         self.makeSPPH(
-            distroseries=distroseries.parent_series,
-            sourcepackagerelease=parent_spr)
+            distroseries=dsp.parent_series, sourcepackagerelease=parent_spr)
         script = self.makeScript([
             '--distribution', distroseries.distribution.name,
             '--series', distroseries.name,

=== modified file 'lib/lp/registry/stories/distroseries/xx-distroseries-index.txt'
--- lib/lp/registry/stories/distroseries/xx-distroseries-index.txt	2011-04-11 00:45:29 +0000
+++ lib/lp/registry/stories/distroseries/xx-distroseries-index.txt	2011-05-09 09:35:49 +0000
@@ -51,7 +51,7 @@
     Release manager: None
     Status: Current Stable Release
     Derives from: Warty (4.10) is not derived from another series.
-    Derived series: Hoary Mock (9.9.9), Breezy Badger Autotest (6.6.6)
+    Derived series:
     Source packages: 3
     Binary packages: 4
 
@@ -69,7 +69,7 @@
     Release manager: Jeff Waugh
     Status: Pre-release Freeze
     Derives from: Woody (3.0)
-    Derived series: No derived series.
+    Derived series:
     Source packages: No sources imported or published.
     Binary packages: No binaries imported or published.
 

=== modified file 'lib/lp/registry/templates/distroseries-details.pt'
--- lib/lp/registry/templates/distroseries-details.pt	2011-04-11 00:45:29 +0000
+++ lib/lp/registry/templates/distroseries-details.pt	2011-05-09 09:35:49 +0000
@@ -70,7 +70,7 @@
             tal:content="child_series/named_version" /><tal:comma
             condition="not: repeat/child_series/end">,</tal:comma>
         </tal:per_child_series>
-       <tal:none condition="all_child_series/is_empty">
+       <tal:none condition="all_child_series">
           No derived series.
         </tal:none>
       </dd>

=== modified file 'lib/lp/registry/templates/distroseries-localdifferences.pt'
--- lib/lp/registry/templates/distroseries-localdifferences.pt	2011-05-05 13:01:50 +0000
+++ lib/lp/registry/templates/distroseries-localdifferences.pt	2011-05-09 09:35:49 +0000
@@ -23,7 +23,7 @@
     <div class="top-portlet" metal:fill-slot="main"
       tal:define="differences view/cached_differences;
                   series_name context/displayname;
-                  parent_name context/parent_series/displayname;
+                  parent_name view/parent_series/displayname;
                   can_perform_sync view/canPerformSync;">
       <p><tal:replace replace="structure view/explanation/escapedtext" /></p>
 

=== modified file 'lib/lp/registry/templates/distroseries-portlet-derivation.pt'
--- lib/lp/registry/templates/distroseries-portlet-derivation.pt	2011-04-22 10:48:57 +0000
+++ lib/lp/registry/templates/distroseries-portlet-derivation.pt	2011-05-09 09:35:49 +0000
@@ -6,7 +6,7 @@
   tal:define="overview_menu context/menu:overview">
   <tal:is_derived condition="context/is_derived_series">
     <tal:is_initialised condition="not: context/is_initialising">
-    <h2>Derived from <tal:name replace="context/parent_series/displayname"/></h2>
+    <h2>Derived from <tal:name replace="view/parent_series/displayname"/></h2>
 
       <tal:diffs define="nb_diffs view/num_differences;
                          nb_diffs_in_parent view/num_differences_in_parent;
@@ -28,7 +28,7 @@
               <tal:nb_diffs replace="nb_diffs_in_parent"/> package<tal:plural
                 content="string:s"
                 condition="python:nb_diffs_in_parent!=1"/> only in <tal:replace
-                  replace="context/parent_series/displayname">Sid</tal:replace>
+                  replace="view/parent_series/displayname">Sid</tal:replace>
             </a>
           </li>
           <li tal:condition="nb_diffs_in_child">

=== modified file 'lib/lp/registry/templates/distroseriesdifference-listing-extra.pt'
--- lib/lp/registry/templates/distroseriesdifference-listing-extra.pt	2011-04-20 09:23:44 +0000
+++ lib/lp/registry/templates/distroseriesdifference-listing-extra.pt	2011-05-09 09:35:49 +0000
@@ -14,7 +14,7 @@
 
     <metal:macro-parent metal:define-macro="base-to-parent">
       <span tal:replace="context/base_version">1.1.1</span> to
-      <span tal:replace="context/derived_series/parent_series/displayname">
+      <span tal:replace="context/parent_series/displayname">
         Lucid</span> version:
       <span tal:replace="context/parent_source_version">1.2.4</span>
     </metal:macro-parent>

=== modified file 'lib/lp/registry/tests/test_distroseries.py'
--- lib/lp/registry/tests/test_distroseries.py	2011-04-13 09:55:48 +0000
+++ lib/lp/registry/tests/test_distroseries.py	2011-05-09 09:35:49 +0000
@@ -209,10 +209,9 @@
                 processorfamily.processors[0]))
 
     def test_getDerivedSeries(self):
-        distroseries = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
-        self.assertContentEqual(
-            [distroseries], distroseries.parent_series.getDerivedSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        self.assertEquals(
+            [dsp.derived_series], dsp.parent_series.getDerivedSeries())
 
     def test_registrant_owner_differ(self):
         # The registrant is the creator whereas the owner is the
@@ -223,14 +222,6 @@
         self.assertEquals(registrant, distroseries.registrant)
         self.assertNotEqual(distroseries.registrant, distroseries.owner)
 
-    def test_is_derived(self):
-        # The series is a derived series if it has a parent_series set.
-        derived_distroseries = self.factory.makeDistroRelease(
-            parent_series=self.factory.makeDistroRelease())
-        distroseries = self.factory.makeDistroRelease()
-        self.assertFalse(distroseries.is_derived_series)
-        self.assertTrue(derived_distroseries.is_derived_series)
-
     def test_is_initialising(self):
         # The series is_initialising only if there is an initialisation
         # job with a pending status attached to this series.

=== modified file 'lib/lp/registry/tests/test_distroseriesdifference.py'
--- lib/lp/registry/tests/test_distroseriesdifference.py	2011-04-21 16:11:51 +0000
+++ lib/lp/registry/tests/test_distroseriesdifference.py	2011-05-09 09:35:49 +0000
@@ -1,4 +1,4 @@
-# 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).
 
 """Model tests for the DistroSeriesDifference class."""
@@ -111,8 +111,7 @@
         self.assertEqual(
             'foonew', ds_diff.parent_source_pub.source_package_name)
         self.assertEqual(
-            ds_diff.derived_series.parent_series,
-            ds_diff.parent_source_pub.distroseries)
+            ds_diff.parent_series, ds_diff.parent_source_pub.distroseries)
 
     def test_parent_source_pub_gets_latest_pending(self):
         # The most recent publication is always returned, even if its pending.
@@ -120,7 +119,7 @@
             source_package_name_str="foonew")
         pending_pub = self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=ds_diff.source_package_name,
-            distroseries=ds_diff.derived_series.parent_series,
+            distroseries=ds_diff.parent_series,
             status=PackagePublishingStatus.PENDING)
 
         self.assertEqual(pending_pub, ds_diff.parent_source_pub)
@@ -150,7 +149,7 @@
                 'parent': '1.0',
                 'derived': '0.9',
                 })
-        new_derived_pub = self.factory.makeSourcePackagePublishingHistory(
+        self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=ds_diff.source_package_name,
             distroseries=ds_diff.derived_series,
             status=PackagePublishingStatus.PENDING,
@@ -171,7 +170,7 @@
             versions=['1.0', '1.2'])
         parent_changelog = self.factory.makeChangelog(
             versions=['1.0', '1.3'])
-        transaction.commit() # Yay, librarian.
+        transaction.commit()  # Yay, librarian.
         ds_diff = self.factory.makeDistroSeriesDifference(versions={
             'derived': '1.2',
             'parent': '1.3',
@@ -190,14 +189,14 @@
 
         # Resolve the DSD by making the same package version published
         # in parent and derived.
-        new_derived_pub = self.factory.makeSourcePackagePublishingHistory(
+        self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=ds_diff.source_package_name,
             distroseries=ds_diff.derived_series,
             status=PackagePublishingStatus.PENDING,
             version='1.4')
-        new_parent_pub = self.factory.makeSourcePackagePublishingHistory(
+        self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=ds_diff.source_package_name,
-            distroseries=ds_diff.derived_series.parent_series,
+            distroseries=ds_diff.parent_series,
             status=PackagePublishingStatus.PENDING,
             version='1.4')
 
@@ -219,7 +218,7 @@
                 'derived': '1.0',
                 },
             status=DistroSeriesDifferenceStatus.RESOLVED)
-        new_derived_pub = self.factory.makeSourcePackagePublishingHistory(
+        self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=ds_diff.source_package_name,
             distroseries=ds_diff.derived_series,
             status=PackagePublishingStatus.PENDING,
@@ -241,7 +240,7 @@
                 'parent': '1.0',
                 'derived': '0.9',
                 })
-        new_derived_pub = self.factory.makeSourcePackagePublishingHistory(
+        self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=ds_diff.source_package_name,
             distroseries=ds_diff.derived_series,
             status=PackagePublishingStatus.PENDING,
@@ -267,9 +266,9 @@
                 },
             difference_type=(
                 DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES))
-        new_parent_pub = self.factory.makeSourcePackagePublishingHistory(
+        self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=ds_diff.source_package_name,
-            distroseries=ds_diff.derived_series.parent_series,
+            distroseries=ds_diff.parent_series,
             status=PackagePublishingStatus.PENDING,
             version='1.1')
 
@@ -291,7 +290,7 @@
             difference_type=(
                 DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES),
             status=DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
-        new_derived_pub = self.factory.makeSourcePackagePublishingHistory(
+        self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=ds_diff.source_package_name,
             distroseries=ds_diff.derived_series,
             status=PackagePublishingStatus.PENDING,
@@ -316,7 +315,7 @@
                 'parent': '1.0',
                 },
             status=DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS)
-        new_derived_pub = self.factory.makeSourcePackagePublishingHistory(
+        self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=ds_diff.source_package_name,
             distroseries=ds_diff.derived_series,
             status=PackagePublishingStatus.PENDING,
@@ -332,8 +331,9 @@
     def test_title(self):
         # The title is a friendly description of the difference.
         parent_series = self.factory.makeDistroSeries(name="lucid")
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=parent_series, name="derilucid")
+        derived_series = self.factory.makeDistroSeries(name="derilucid")
+        self.factory.makeDistroSeriesParent(
+            derived_series=derived_series, parent_series=parent_series)
         ds_diff = self.factory.makeDistroSeriesDifference(
             source_package_name_str="foonew", derived_series=derived_series,
             versions={
@@ -395,8 +395,7 @@
         person = self.factory.makePerson()
         with person_logged_in(person):
             self.assertTrue(check_permission('launchpad.Edit', ds_diff))
-            diff_comment = ds_diff.addComment(
-                ds_diff.derived_series.owner, "Boo")
+            ds_diff.addComment(ds_diff.derived_series.owner, "Boo")
 
     def _setupPackageSets(self, ds_diff, distroseries, nb_packagesets):
         # Helper method to create packages sets.
@@ -413,7 +412,7 @@
         # All parent's packagesets are returned ordered alphabetically.
         ds_diff = self.factory.makeDistroSeriesDifference()
         packagesets = self._setupPackageSets(
-            ds_diff, ds_diff.derived_series.parent_series, 5)
+            ds_diff, ds_diff.parent_series, 5)
         parent_packagesets = ds_diff.getParentPackageSets()
         self.assertEquals(
             sorted([packageset.name for packageset in packagesets]),
@@ -470,7 +469,7 @@
                 'parent': '1.0',
                 },
             status=DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS)
-        new_derived_pub = self.factory.makeSourcePackagePublishingHistory(
+        self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=ds_diff.source_package_name,
             distroseries=ds_diff.derived_series,
             status=PackagePublishingStatus.PENDING,
@@ -508,16 +507,15 @@
     def test_base_version_none(self):
         # The attribute is set to None if there is no common base version.
         # Publish different versions in the series.
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
         source_package_name = self.factory.getOrMakeSourcePackageName('foo')
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=derived_series,
+            distroseries=dsp.derived_series,
             version='1.0deri1',
             sourcepackagename=source_package_name,
             status=PackagePublishingStatus.PUBLISHED)
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=derived_series.parent_series,
+            distroseries=dsp.parent_series,
             version='1.0ubu2',
             sourcepackagename=source_package_name,
             status=PackagePublishingStatus.PUBLISHED)
@@ -527,16 +525,15 @@
 
     def test_base_version_multiple(self):
         # The latest common base version is set as the base-version.
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
-        source_package_name = self.factory.getOrMakeSourcePackageName('foo')
+        dsp = self.factory.makeDistroSeriesParent()
+        self.factory.getOrMakeSourcePackageName('foo')
         # Create changelogs for both.
         changelog_lfa = self.factory.makeChangelog('foo', ['1.2', '1.1'])
         parent_changelog_lfa = self.factory.makeChangelog('foo', ['1.1'])
-        transaction.commit() # Yay, librarian.
+        transaction.commit()  # Yay, librarian.
 
         ds_diff = self.factory.makeDistroSeriesDifference(
-            derived_series=derived_series, source_package_name_str='foo',
+            derived_series=dsp.derived_series, source_package_name_str='foo',
             versions={
                 'derived': '1.2',
                 'parent': '1.3',
@@ -550,18 +547,17 @@
     def test_base_version_invalid(self):
         # If the maximum base version is invalid, it is discarded and not
         # set as the base version.
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
-        source_package_name = self.factory.getOrMakeSourcePackageName('foo')
+        dsp = self.factory.makeDistroSeriesParent()
+        self.factory.getOrMakeSourcePackageName('foo')
         # Create changelogs for both.
         changelog_lfa = self.factory.makeChangelog(
             'foo', ['1:2.0-1', 'a1:1.8.8-070403-1~priv1', '1:1.7-1'])
         parent_changelog_lfa = self.factory.makeChangelog(
             'foo', ['1:2.0-2', 'a1:1.8.8-070403-1~priv1', '1:1.7-1'])
-        transaction.commit() # Yay, librarian.
+        transaction.commit()  # Yay, librarian.
 
         ds_diff = self.factory.makeDistroSeriesDifference(
-            derived_series=derived_series, source_package_name_str='foo',
+            derived_series=dsp.derived_series, source_package_name_str='foo',
             versions={
                 'derived': '1:2.0-1',
                 'parent': '1:2.0-2',
@@ -586,7 +582,7 @@
             versions=['1.0', '1.2'])
         parent_changelog = self.factory.makeChangelog(
             versions=['1.0', '1.3'])
-        transaction.commit() # Yay, librarian.
+        transaction.commit()  # Yay, librarian.
 
         ds_diff = self.factory.makeDistroSeriesDifference(versions={
             'derived': '1.2',
@@ -600,8 +596,7 @@
 
         base_pub = ds_diff.base_source_pub
         self.assertEqual('1.0', base_pub.source_package_version)
-        self.assertEqual(
-            ds_diff.derived_series.parent_series, base_pub.distroseries)
+        self.assertEqual(ds_diff.parent_series, base_pub.distroseries)
 
     def test_base_source_pub_not_published(self):
         # If the base version isn't published, the base version is
@@ -610,7 +605,7 @@
             versions=['1.0', '1.2'])
         parent_changelog = self.factory.makeChangelog(
             versions=['1.0', '1.3'])
-        transaction.commit() # Yay, librarian.
+        transaction.commit()  # Yay, librarian.
 
         ds_diff = self.factory.makeDistroSeriesDifference(versions={
             'derived': '1.2',
@@ -630,7 +625,7 @@
             versions=['1.0', '1.2'])
         parent_changelog = self.factory.makeChangelog(
             versions=['1.0', '1.3'])
-        transaction.commit() # Yay, librarian.
+        transaction.commit()  # Yay, librarian.
 
         ds_diff = self.factory.makeDistroSeriesDifference(
             versions={
@@ -660,12 +655,12 @@
         # {derived,parent}_versions must be ordered (e.g. ['1.1',
         # '1.2', '1.3']).
         if status is None:
-            status=DistroSeriesDifferenceStatus.NEEDS_ATTENTION
+            status = DistroSeriesDifferenceStatus.NEEDS_ATTENTION
         derived_changelog = self.factory.makeChangelog(
             versions=derived_versions)
         parent_changelog = self.factory.makeChangelog(
             versions=parent_versions)
-        transaction.commit() # Yay, librarian.
+        transaction.commit()  # Yay, librarian.
         ds_diff = self.factory.makeDistroSeriesDifference(
             status=status,
             versions={
@@ -743,24 +738,23 @@
     def test_source_package_release_pending(self):
         # source_package_release returns the package release of version
         # source_version with status PUBLISHED or PENDING.
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
         source_package_name = self.factory.getOrMakeSourcePackageName('foo')
         versions = {'derived': u'1.2', 'parent': u'1.3'}
 
         ds_diff = self.factory.makeDistroSeriesDifference(
-            derived_series=derived_series,
+            derived_series=dsp.derived_series,
             source_package_name_str=source_package_name.name,
             versions=versions)
 
         # Create pending source package releases.
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=derived_series,
+            distroseries=dsp.derived_series,
             version='1.4',
             sourcepackagename=source_package_name,
             status=PackagePublishingStatus.PENDING)
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=derived_series.parent_series,
+            distroseries=dsp.parent_series,
             version='1.5',
             sourcepackagename=source_package_name,
             status=PackagePublishingStatus.PENDING)
@@ -778,7 +772,7 @@
     def createPublication(self, spn, versions, distroseries,
                           status=PackagePublishingStatus.PUBLISHED):
         changelog_lfa = self.factory.makeChangelog(spn.name, versions)
-        transaction.commit() # Yay, librarian.
+        transaction.commit()  # Yay, librarian.
         spr = self.factory.makeSourcePackageRelease(
             sourcepackagename=spn, version=versions[0],
             changelog=changelog_lfa)
@@ -789,16 +783,15 @@
     def test_existing_packagediff_is_linked_when_dsd_created(self):
         # When a relevant packagediff already exists, it is linked to the
         # DSD when it is created.
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
         spn = self.factory.getOrMakeSourcePackageName(
             name=self.factory.getUniqueString())
-        parent_spph = self.createPublication(
-            spn, ['1.2-1', '1.0-1'], derived_series.parent_series)
+        self.createPublication(
+            spn, ['1.2-1', '1.0-1'], dsp.parent_series)
         spph = self.createPublication(
-            spn, ['1.1-1', '1.0-1'], derived_series)
+            spn, ['1.1-1', '1.0-1'], dsp.derived_series)
         base_spph = self.createPublication(
-            spn, ['1.0-1'], derived_series,
+            spn, ['1.0-1'], dsp.derived_series,
             status=PackagePublishingStatus.SUPERSEDED)
         pd = self.factory.makePackageDiff(
             from_source=base_spph.sourcepackagerelease,
@@ -806,13 +799,13 @@
         # factory.makeDistroSeriesDifference() will always create
         # publications to be helpful. We don't need the help in this case.
         dsd = getUtility(IDistroSeriesDifferenceSource).new(
-            derived_series, spn)
+            dsp.derived_series, spn)
         self.assertEqual(pd, dsd.package_diff)
 
     def _initDiffWithMultiplePendingPublications(self, versions, parent):
         ds_diff = self.factory.makeDistroSeriesDifference(versions=versions)
         if parent:
-            series = ds_diff.derived_series.parent_series
+            series = ds_diff.parent_series
             version = versions.get('parent')
         else:
             series = ds_diff.derived_series
@@ -876,7 +869,7 @@
 
         verifyObject(IDistroSeriesDifferenceSource, dsd_source)
 
-    def makeDiffsForDistroSeries(self, derived_series):
+    def makeDiffsForDistroSeries(self, derived_series, parent_series=None):
         # Helper that creates a range of differences for a derived
         # series.
         diffs = {
@@ -886,22 +879,53 @@
             }
         diffs['normal'].append(
             self.factory.makeDistroSeriesDifference(
-                derived_series=derived_series))
+                derived_series=derived_series, parent_series=parent_series))
         diffs['unique'].append(
             self.factory.makeDistroSeriesDifference(
                 derived_series=derived_series,
+                parent_series=parent_series,
                 difference_type=(
                     DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES)))
         diffs['ignored'].append(
             self.factory.makeDistroSeriesDifference(
                 derived_series=derived_series,
+                parent_series=parent_series,
                 status=DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT))
         return diffs
 
-    def makeDerivedSeries(self):
+    def makeDerivedSeries(self, derived_series=None):
         # Keep tests DRY.
-        return self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent(
+            derived_series=derived_series)
+        return dsp.derived_series
+
+    def makeVersionDifference(self, derived_series=None, changed_parent=False,
+                              changed_child=False, status=None):
+        """Create a `DistroSeriesDifference` between package versions.
+
+        The differing package will exist in both the parent series and in the
+        child.
+
+        :param derived_series: Optional `DistroSeries` that the difference is
+            for.  If not given, one will be created.
+        :param changed_parent: Whether the difference should show a change in
+            the parent's version of the package.
+        :param changed_child: Whether the difference should show a change in
+            the child's version of the package.
+        :param status: Optional status for the `DistroSeriesDifference`.  If
+            not given, defaults to `NEEDS_ATTENTION`.
+        """
+        if status is None:
+            status = DistroSeriesDifferenceStatus.NEEDS_ATTENTION
+        base_version = "1.%d" % self.factory.getUniqueInteger()
+        versions = dict.fromkeys(('base', 'parent', 'derived'), base_version)
+        if changed_parent:
+            versions['parent'] += "-%s" % self.factory.getUniqueString()
+        if changed_child:
+            versions['derived'] += "-%s" % self.factory.getUniqueString()
+        return self.factory.makeDistroSeriesDifference(
+            derived_series=derived_series, versions=versions, status=status,
+            set_base_version=True)
 
     def test_getForDistroSeries_default(self):
         # By default all differences needing attention for the given
@@ -918,7 +942,7 @@
     def test_getForDistroSeries_filters_by_distroseries(self):
         # Differences for other series are not included.
         derived_series = self.makeDerivedSeries()
-        diffs = self.makeDiffsForDistroSeries(derived_series)
+        self.makeDiffsForDistroSeries(derived_series)
         diff_for_other_series = self.factory.makeDistroSeriesDifference()
 
         result = getUtility(IDistroSeriesDifferenceSource).getForDistroSeries(
@@ -929,12 +953,14 @@
     def test_getForDistroSeries_filters_by_type(self):
         # Only differences for the specified types are returned.
         derived_series = self.makeDerivedSeries()
-        diffs = self.makeDiffsForDistroSeries(derived_series)
+        self.makeDiffsForDistroSeries(derived_series)
 
-        result = getUtility(IDistroSeriesDifferenceSource).getForDistroSeries(
+        getUtility(IDistroSeriesDifferenceSource).getForDistroSeries(
             derived_series,
             DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES)
 
+        self.assertContentEqual(diffs['unique'], result)
+
     def test_getForDistroSeries_filters_by_status(self):
         # A single status can be used to filter results.
         derived_series = self.makeDerivedSeries()
@@ -976,6 +1002,31 @@
             sorted(names),
             [result.source_package_name.name for result in results])
 
+    def test_getForDistroSeries_filters_by_parent(self):
+        # The differences can be filtered by parent series.
+        dsp = self.factory.makeDistroSeriesParent()
+        derived_series = dsp.derived_series
+        parent_series = dsp.parent_series
+
+        # Add another parent to this series.
+        parent_series2 = self.factory.makeDistroSeriesParent(
+            derived_series=derived_series).parent_series
+
+        diffs = self.makeDiffsForDistroSeries(
+            derived_series, parent_series=parent_series)
+        diffs2 = self.makeDiffsForDistroSeries(
+            derived_series, parent_series=parent_series2)
+
+        results = getUtility(
+            IDistroSeriesDifferenceSource).getForDistroSeries(
+                derived_series, parent_series=parent_series)
+        results2 = getUtility(
+            IDistroSeriesDifferenceSource).getForDistroSeries(
+                derived_series, parent_series=parent_series2)
+
+        self.assertContentEqual(diffs['normal'], results)
+        self.assertContentEqual(diffs2['normal'], results2)
+
     def test_getByDistroSeriesAndName(self):
         # An individual difference is obtained using the name.
         ds_diff = self.factory.makeDistroSeriesDifference(
@@ -987,17 +1038,89 @@
 
         self.assertEqual(ds_diff, result)
 
+    def test_getSimpleUpgrades_finds_simple_update(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        dsd = self.makeVersionDifference(changed_parent=True)
+        self.assertEqual(dsd.base_version, dsd.source_version)
+        self.assertContentEqual(
+            [dsd], dsd_source.getSimpleUpgrades(dsd.derived_series))
+
+    def test_getSimpleUpgrades_ignores_hidden_differences(self):
+        invisible_statuses = [
+            DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT,
+            DistroSeriesDifferenceStatus.BLACKLISTED_ALWAYS,
+            DistroSeriesDifferenceStatus.RESOLVED,
+            ]
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        series = self.makeDerivedSeries()
+        for status in invisible_statuses:
+            self.makeVersionDifference(
+                derived_series=series, changed_parent=True, status=status)
+        self.assertContentEqual([], dsd_source.getSimpleUpgrades(series))
+
+    def test_getSimpleUpgrades_ignores_other_distroseries(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        self.makeVersionDifference(changed_parent=True)
+        self.assertContentEqual(
+            [], dsd_source.getSimpleUpgrades(self.factory.makeDistroSeries()))
+
+    def test_getSimpleUpgrades_ignores_packages_changed_in_child(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        dsd = self.makeVersionDifference(
+            changed_parent=True, changed_child=True)
+        self.assertContentEqual(
+            [], dsd_source.getSimpleUpgrades(dsd.derived_series))
+
+    def test_getSimpleUpgrades_ignores_packages_not_updated_in_parent(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        dsd = self.makeVersionDifference(changed_parent=False)
+        self.assertContentEqual(
+            [], dsd_source.getSimpleUpgrades(dsd.derived_series))
+
+    def test_getSimpleUpgrades_ignores_packages_unique_to_child(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        diff_type = DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES
+        dsd = self.factory.makeDistroSeriesDifference(
+            difference_type=diff_type)
+        self.assertContentEqual(
+            [], dsd_source.getSimpleUpgrades(dsd.derived_series))
+
+    def test_getSimpleUpgrades_ignores_packages_missing_from_child(self):
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        diff_type = DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES
+        dsd = self.factory.makeDistroSeriesDifference(
+            difference_type=diff_type)
+        self.assertContentEqual(
+            [], dsd_source.getSimpleUpgrades(dsd.derived_series))
+
+    def test_collateDifferencesByParentArchive(self):
+        dsp1 = self.factory.makeDistroSeriesParent()
+        dsp2 = self.factory.makeDistroSeriesParent()
+        differences = [
+            self.factory.makeDistroSeriesDifference(dsp1.derived_series),
+            self.factory.makeDistroSeriesDifference(dsp2.derived_series),
+            self.factory.makeDistroSeriesDifference(dsp1.derived_series),
+            self.factory.makeDistroSeriesDifference(dsp2.derived_series),
+            ]
+        dsd_source = getUtility(IDistroSeriesDifferenceSource)
+        observed = (
+            dsd_source.collateDifferencesByParentArchive(differences))
+        expected = {
+            dsp1.parent_series.main_archive: differences[0::2],
+            dsp2.parent_series.main_archive: differences[1::2],
+            }
+        self.assertEqual(observed, expected)
+
 
 class TestMostRecentComments(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer
 
     def test_most_recent_comments(self):
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
         dsds = set(
             self.factory.makeDistroSeriesDifference(
-                derived_series=derived_series) for index in xrange(5))
+                derived_series=dsp.derived_series) for index in xrange(5))
         expected_comments = set()
         for dsd in dsds:
             # Add a couple of comments.
@@ -1028,8 +1151,8 @@
         return dsd
 
     def test_simple(self):
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        derived_series = dsp.derived_series
         dsds = [
             self.create_difference(derived_series),
             self.create_difference(derived_series),
@@ -1058,8 +1181,8 @@
             parent_source_pubs_by_spn_id_found)
 
     def test_statuses(self):
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        derived_series = dsp.derived_series
         dsd = self.create_difference(derived_series)
         # Change the derived source publication to DELETED.
         removeSecurityProxy(dsd.source_pub).status = (
@@ -1080,8 +1203,8 @@
         # When match_version is True, the version of the publications (well,
         # the release) must exactly match those recorded on the
         # DistroSeriesDifference.
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        derived_series = dsp.derived_series
         dsd = self.create_difference(derived_series)
         # Modify the release version.
         removeSecurityProxy(

=== modified file 'lib/lp/soyuz/model/distroseriesdifferencejob.py'
--- lib/lp/soyuz/model/distroseriesdifferencejob.py	2011-04-13 15:20:12 +0000
+++ lib/lp/soyuz/model/distroseriesdifferencejob.py	2011-05-09 09:35:49 +0000
@@ -18,6 +18,7 @@
 from lp.registry.interfaces.distroseriesdifference import (
     IDistroSeriesDifferenceSource,
     )
+from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.model.distroseriesdifference import DistroSeriesDifference
 from lp.registry.model.sourcepackagename import SourcePackageName
@@ -92,12 +93,14 @@
     """
     if distroseries is None:
         return False
-    parent_series = distroseries.parent_series
-    if parent_series is None:
-        return False
-    if parent_series.distribution == distroseries.distribution:
-        # Differences within a distribution are not tracked.
-        return False
+    dsp = getUtility(IDistroSeriesParentSet).getByDerivedSeries(
+        distroseries)
+    if dsp.count() == 0:
+        return False
+    for parent in dsp:
+        if parent.parent_series.distribution == distroseries.distribution:
+            # Differences within a distribution are not tracked.
+            return False
     return find_waiting_jobs(distroseries, sourcepackagename).is_empty()
 
 
@@ -148,7 +151,9 @@
         in a packageset that the derived series also has.
         """
         derived_series = self.distroseries
-        parent_series = derived_series.parent_series
+        dsp = getUtility(IDistroSeriesParentSet).getByDerivedSeries(
+            derived_series)
+        parent_series = dsp[0].parent_series
         if has_package(derived_series, self.sourcepackagename):
             return True
         if not has_package(parent_series, self.sourcepackagename):

=== modified file 'lib/lp/soyuz/tests/test_distroseriesdifferencejob.py'
--- lib/lp/soyuz/tests/test_distroseriesdifferencejob.py	2011-04-18 05:25:38 +0000
+++ lib/lp/soyuz/tests/test_distroseriesdifferencejob.py	2011-05-09 09:35:49 +0000
@@ -52,8 +52,8 @@
         return getUtility(IDistroSeriesDifferenceJobSource)
 
     def makeDerivedDistroSeries(self):
-        return self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        return dsp.derived_series
 
     def test_baseline(self):
         verifyObject(IDistroSeriesDifferenceJobSource, self.getJobSource())
@@ -147,23 +147,24 @@
             [], find_waiting_jobs(distroseries, sourcepackagename))
 
     def test_createForPackagedPublication_creates_jobs_for_its_child(self):
-        derived_series = self.factory.makeDistroSeries(
-            parent_series=self.makeDerivedDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        parent_dsp = self.factory.makeDistroSeriesParent(
+            derived_series=dsp.parent_series)
         package = self.factory.makeSourcePackageName()
         # Create a job for the derived_series parent, which should create
         # two jobs. One for derived_series, and the other for its child.
         self.getJobSource().createForPackagePublication(
-            derived_series.parent_series, package,
+            dsp.parent_series, package,
             PackagePublishingPocket.RELEASE)
         jobs = (list(
-            find_waiting_jobs(derived_series.parent_series, package)) +
-            list(find_waiting_jobs(derived_series, package)))
+            find_waiting_jobs(dsp.parent_series, package)) +
+            list(find_waiting_jobs(dsp.derived_series, package)))
         self.assertEqual(2, len(jobs))
         self.assertEqual(package.id, jobs[0].metadata['sourcepackagename'])
         self.assertEqual(package.id, jobs[1].metadata['sourcepackagename'])
         # Lastly, a job was not created for the grandparent.
         jobs = list(
-            find_waiting_jobs(derived_series.parent_series.parent_series,
+            find_waiting_jobs(parent_dsp.parent_series,
                 package))
         self.assertEqual(0, len(jobs))
 
@@ -242,82 +243,77 @@
         self.assertEqual(1, ds_diff.count())
 
     def test_packageset_filter_passes_inherited_packages(self):
-        derived_series = self.makeDerivedDistroSeries()
-        parent_series = derived_series.parent_series
+        dsp = self.factory.makeDistroSeriesParent()
         # Parent must have a packageset or the filter will pass anyway.
-        self.factory.makePackageset(distroseries=parent_series)
+        self.factory.makePackageset(distroseries=dsp.parent_series)
         package = self.factory.makeSourcePackageName()
         # Package is not in the packageset _but_ both the parent and
         # derived series have it.
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=parent_series, sourcepackagename=package)
+            distroseries=dsp.parent_series, sourcepackagename=package)
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=derived_series, sourcepackagename=package)
-        job = create_job(derived_series, package)
+            distroseries=dsp.derived_series, sourcepackagename=package)
+        job = create_job(dsp.derived_series, package)
         self.assertTrue(job.passesPackagesetFilter())
 
     def test_packageset_filter_passes_packages_unique_to_derived_series(self):
-        derived_series = self.makeDerivedDistroSeries()
-        parent_series = derived_series.parent_series
+        dsp = self.factory.makeDistroSeriesParent()
         # Parent must have a packageset or the filter will pass anyway.
-        self.factory.makePackageset(distroseries=parent_series)
+        self.factory.makePackageset(distroseries=dsp.parent_series)
         package = self.factory.makeSourcePackageName()
         # Package exists in the derived series but not in the parent
         # series.
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=derived_series, sourcepackagename=package)
-        job = create_job(derived_series, package)
+            distroseries=dsp.derived_series, sourcepackagename=package)
+        job = create_job(dsp.derived_series, package)
         self.assertTrue(job.passesPackagesetFilter())
 
     def test_packageset_filter_passes_all_if_parent_has_no_packagesets(self):
         # Debian in particular has no packagesets.  If the parent series
         # has no packagesets, the packageset filter passes all packages.
-        derived_series = self.makeDerivedDistroSeries()
-        parent_series = derived_series.parent_series
+        dsp = self.factory.makeDistroSeriesParent()
         package = self.factory.makeSourcePackageName()
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=parent_series, sourcepackagename=package)
-        job = create_job(derived_series, package)
+            distroseries=dsp.parent_series, sourcepackagename=package)
+        job = create_job(dsp.derived_series, package)
         self.assertTrue(job.passesPackagesetFilter())
 
-    def makeInheritedPackageSet(self, derived_series, packages=()):
+    def makeInheritedPackageSet(self, distro_series_parent, packages=()):
         """Simulate an inherited `Packageset`.
 
         Creates a packageset in the parent that has an equivalent in
         `derived_series`.
         """
-        parent_series = derived_series.parent_series
         parent_packageset = self.factory.makePackageset(
-            distroseries=parent_series, packages=packages)
+            distroseries=distro_series_parent.parent_series,
+            packages=packages)
         derived_packageset = self.factory.makePackageset(
-            distroseries=derived_series, packages=packages,
-            name=parent_packageset.name, owner=parent_packageset.owner,
-            related_set=parent_packageset)
+            distroseries=distro_series_parent.derived_series,
+            packages=packages, name=parent_packageset.name,
+            owner=parent_packageset.owner, related_set=parent_packageset)
 
     def test_packageset_filter_passes_package_in_inherited_packageset(self):
-        derived_series = self.makeDerivedDistroSeries()
-        parent_series = derived_series.parent_series
+        dsp = self.factory.makeDistroSeriesParent()
         # Package is in a packageset on the parent that the derived
         # series also has.
         package = self.factory.makeSourcePackageName()
-        self.makeInheritedPackageSet(derived_series, [package])
+        self.makeInheritedPackageSet(dsp, [package])
         # Package is in parent series and in a packageset that the
         # derived series inherited.
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=parent_series, sourcepackagename=package)
-        job = create_job(derived_series, package)
+            distroseries=dsp.parent_series, sourcepackagename=package)
+        job = create_job(dsp.derived_series, package)
         self.assertTrue(job.passesPackagesetFilter())
 
     def test_packageset_filter_blocks_unwanted_parent_package(self):
-        derived_series = self.makeDerivedDistroSeries()
-        parent_series = derived_series.parent_series
-        self.makeInheritedPackageSet(derived_series)
+        dsp = self.factory.makeDistroSeriesParent()
+        self.makeInheritedPackageSet(dsp)
         package = self.factory.makeSourcePackageName()
         # Package is in the parent series but not in a packageset shared
         # between the derived series and the parent series.
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=parent_series, sourcepackagename=package)
-        job = create_job(derived_series, package)
+            distroseries=dsp.parent_series, sourcepackagename=package)
+        job = create_job(dsp.derived_series, package)
         self.assertFalse(job.passesPackagesetFilter())
 
 
@@ -334,8 +330,8 @@
         return getUtility(IDistroSeriesDifferenceJobSource)
 
     def makeDerivedDistroSeries(self):
-        return self.factory.makeDistroSeries(
-            parent_series=self.factory.makeDistroSeries())
+        dsp = self.factory.makeDistroSeriesParent()
+        return dsp
 
     def createPublication(self, source_package_name, versions, distroseries,
                           archive=None):
@@ -373,12 +369,13 @@
     def test_parent_gets_newer(self):
         # When a new source package is uploaded to the parent distroseries,
         # a job is created that updates the relevant DSD.
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         self.createPublication(
             source_package_name, ['1.0-1derived1', '1.0-1'], derived_series)
         self.createPublication(
-            source_package_name, ['1.0-1'], derived_series.parent_series)
+            source_package_name, ['1.0-1'], dsp.parent_series)
         # Creating the SPPHs has created jobs for us, so grab it off the
         # queue.
         jobs = find_waiting_jobs(derived_series, source_package_name)
@@ -391,7 +388,7 @@
         # Now create a 1.0-2 upload to the parent.
         self.createPublication(
             source_package_name, ['1.0-2', '1.0-1'],
-            derived_series.parent_series)
+            dsp.parent_series)
         jobs = find_waiting_jobs(derived_series, source_package_name)
         self.runJob(jobs[0])
         # And the DSD we have a hold of will have updated.
@@ -402,12 +399,13 @@
     def test_child_gets_newer(self):
         # When a new source is uploaded to the child distroseries, the DSD is
         # updated.
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         self.createPublication(
             source_package_name, ['1.0-1'], derived_series)
         self.createPublication(
-            source_package_name, ['1.0-1'], derived_series.parent_series)
+            source_package_name, ['1.0-1'], dsp.parent_series)
         jobs = find_waiting_jobs(derived_series, source_package_name)
         self.runJob(jobs[0])
         ds_diff = self.findDSD(derived_series, source_package_name)
@@ -424,13 +422,13 @@
     def test_child_is_synced(self):
         # If the source package gets 'synced' to the child from the parent,
         # the job correctly updates the DSD.
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         self.createPublication(
             source_package_name, ['1.0-1derived1', '1.0-1'], derived_series)
         self.createPublication(
-            source_package_name, ['1.0-2', '1.0-1'],
-            derived_series.parent_series)
+            source_package_name, ['1.0-2', '1.0-1'], dsp.parent_series)
         jobs = find_waiting_jobs(derived_series, source_package_name)
         self.runJob(jobs[0])
         ds_diff = self.findDSD(derived_series, source_package_name)
@@ -445,7 +443,8 @@
     def test_only_in_child(self):
         # If a source package only exists in the child distroseries, the DSD
         # is created with the right type.
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         self.createPublication(
             source_package_name, ['1.0-0derived1'], derived_series)
@@ -459,11 +458,11 @@
     def test_only_in_parent(self):
         # If a source package only exists in the parent distroseries, the DSD
         # is created with the right type.
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         self.createPublication(
-            source_package_name, ['1.0-1'],
-            derived_series.parent_series)
+            source_package_name, ['1.0-1'], dsp.parent_series)
         jobs = find_waiting_jobs(derived_series, source_package_name)
         self.runJob(jobs[0])
         ds_diff = self.findDSD(derived_series, source_package_name)
@@ -474,12 +473,13 @@
     def test_deleted_in_parent(self):
         # If a source package is deleted in the parent, a job is created, and
         # the DSD is updated correctly.
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         self.createPublication(
             source_package_name, ['1.0-1'], derived_series)
         spph = self.createPublication(
-            source_package_name, ['1.0-1'], derived_series.parent_series)
+            source_package_name, ['1.0-1'], dsp.parent_series)
         jobs = find_waiting_jobs(derived_series, source_package_name)
         self.runJob(jobs[0])
         ds_diff = self.findDSD(derived_series, source_package_name)
@@ -495,12 +495,13 @@
     def test_deleted_in_child(self):
         # If a source package is deleted in the child, a job is created, and
         # the DSD is updated correctly.
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         spph = self.createPublication(
             source_package_name, ['1.0-1'], derived_series)
         self.createPublication(
-            source_package_name, ['1.0-1'], derived_series.parent_series)
+            source_package_name, ['1.0-1'], dsp.parent_series)
         jobs = find_waiting_jobs(derived_series, source_package_name)
         self.runJob(jobs[0])
         ds_diff = self.findDSD(derived_series, source_package_name)
@@ -515,7 +516,8 @@
 
     def test_no_job_for_PPA(self):
         # If a source package is uploaded to a PPA, a job is not created.
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         ppa = self.factory.makeArchive()
         self.createPublication(
@@ -525,7 +527,8 @@
 
     def test_no_job_for_PPA_with_deleted_source(self):
         # If a source package is deleted from a PPA, no job is created.
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         ppa = self.factory.makeArchive()
         spph = self.createPublication(
@@ -536,19 +539,19 @@
 
     def test_update_deletes_diffs(self):
         # When a DSD is updated, the diffs are invalidated.
-        derived_series = self.makeDerivedDistroSeries()
+        dsp = self.makeDerivedDistroSeries()
+        derived_series = dsp.derived_series
         source_package_name = self.factory.makeSourcePackageName()
         self.createPublication(
             source_package_name, ['1.0-1derived1', '1.0-1'], derived_series)
         self.createPublication(
-            source_package_name, ['1.0-2', '1.0-1'],
-            derived_series.parent_series)
+            source_package_name, ['1.0-2', '1.0-1'], dsp.parent_series)
         spr = self.factory.makeSourcePackageRelease(
             sourcepackagename=source_package_name, version='1.0-1')
         self.factory.makeSourcePackagePublishingHistory(
             sourcepackagerelease=spr,
-            archive=derived_series.parent_series.main_archive,
-            distroseries=derived_series.parent_series,
+            archive=dsp.parent_series.main_archive,
+            distroseries=dsp.parent_series,
             status=PackagePublishingStatus.SUPERSEDED)
         jobs = find_waiting_jobs(derived_series, source_package_name)
         self.runJob(jobs[0])
@@ -558,7 +561,7 @@
         self.assertIsNot(None, ds_diff[0].parent_package_diff)
         self.createPublication(
             source_package_name, ['1.0-3', '1.0-2', '1.0-1'],
-            derived_series.parent_series)
+            dsp.parent_series)
         jobs = find_waiting_jobs(derived_series, source_package_name)
         self.runJob(jobs[0])
         # Since the diff showing the changes from 1.0-1 to 1.0-1derived1 is

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-05-03 04:39:43 +0000
+++ lib/lp/testing/factory.py	2011-05-09 09:35:49 +0000
@@ -2314,12 +2314,24 @@
         versions=None,
         difference_type=DistroSeriesDifferenceType.DIFFERENT_VERSIONS,
         status=DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
-        changelogs=None, set_base_version=False):
+        changelogs=None, set_base_version=False, parent_series=None):
         """Create a new distro series source package difference."""
         if derived_series is None:
-            parent_series = self.makeDistroSeries()
-            derived_series = self.makeDistroSeries(
+            dsp = self.makeDistroSeriesParent(
                 parent_series=parent_series)
+            derived_series = dsp.derived_series
+            parent_series = dsp.parent_series
+        else:
+            if parent_series is None:
+                dsp = getUtility(IDistroSeriesParentSet).getByDerivedSeries(
+                    derived_series)
+                if dsp.count() == 0:
+                    new_dsp = self.makeDistroSeriesParent(
+                        derived_series=derived_series,
+                        parent_series=parent_series)
+                    parent_series = new_dsp.parent_series
+                else:
+                    parent_series = dsp[0].parent_series
 
         if source_package_name_str is None:
             source_package_name_str = self.getUniqueString('src-name')
@@ -2334,7 +2346,7 @@
 
         base_version = versions.get('base')
         if base_version is not None:
-            for series in [derived_series, derived_series.parent_series]:
+            for series in [derived_series, parent_series]:
                 spr = self.makeSourcePackageRelease(
                     sourcepackagename=source_package_name,
                     version=base_version)
@@ -2359,12 +2371,12 @@
                 version=versions.get('parent'),
                 changelog=changelogs.get('parent'))
             self.makeSourcePackagePublishingHistory(
-                distroseries=derived_series.parent_series,
+                distroseries=parent_series,
                 sourcepackagerelease=spr,
                 status = PackagePublishingStatus.PUBLISHED)
 
         diff = getUtility(IDistroSeriesDifferenceSource).new(
-            derived_series, source_package_name)
+            derived_series, source_package_name, parent_series)
 
         removeSecurityProxy(diff).status = status