← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/launchpad/dds-add-missingpackages-page2 into lp:launchpad

 

Raphaël Victor Badin has proposed merging lp:~rvb/launchpad/dds-add-missingpackages-page2 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/launchpad/dds-add-missingpackages-page2/+merge/56136

This branch adds a page (+missingpackages) to interact with packages in parent series but not in derived series.

== Implementation details ==
- factor most of the functionalities inside the abstract view class DistroSeriesDifferenceBase.
- use this base class to refactor the view DistroSeriesLocalDifferences and create the view DistroSeriesMissingPackages.
- adapt the template distroseries-localdifferences.pt to use it with the 2 above views.

== QA ==
- Turn on the feature flag :
    'soyuz.derived-series-ui.enabled default 1 on'
- Test the new page (on a derived series)
    http://.../derivedseries/+missingpackages

== Tests ==
./bin/test -cvv test_series_views test_missingpackages_differences
./bin/test -cvv test_series_views test_parent_packagesets_missingpackages
./bin/test -cvv test_series_views test_parent_packagesets_localpackagediffs
./bin/test -cvv test_distroseriesdifference test_getParentPackageSets
./bin/test -cvv test_distroseriesdifference test_getPackageSets
./bin/test -cvv test_distroseriesdifference_views test_packagediffs_display
-- 
https://code.launchpad.net/~rvb/launchpad/dds-add-missingpackages-page2/+merge/56136
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/launchpad/dds-add-missingpackages-page2 into lp:launchpad.
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2011-03-31 12:53:27 +0000
+++ lib/lp/registry/browser/configure.zcml	2011-04-04 11:43:30 +0000
@@ -154,6 +154,12 @@
         class="canonical.launchpad.browser.AskAQuestionButtonView"
         permission="zope.Public"/>
     <browser:page
+        name="+missingpackages"
+        for="lp.registry.interfaces.distroseries.IDistroSeries"
+        class="lp.registry.browser.distroseries.DistroSeriesMissingPackages"
+        template="../templates/distroseries-localdifferences.pt"
+        permission="zope.Public"/>
+    <browser:page
         name="+localpackagediffs"
         for="lp.registry.interfaces.distroseries.IDistroSeries"
         class="lp.registry.browser.distroseries.DistroSeriesLocalDifferences"

=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2011-03-31 12:53:27 +0000
+++ lib/lp/registry/browser/distroseries.py	2011-04-04 11:43:30 +0000
@@ -84,7 +84,10 @@
     add_subscribe_link,
     MilestoneOverlayMixin,
     )
-from lp.registry.enum import DistroSeriesDifferenceStatus
+from lp.registry.enum import (
+    DistroSeriesDifferenceStatus,
+    DistroSeriesDifferenceType,
+    )
 from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.distroseriesdifference import (
     IDistroSeriesDifferenceSource,
@@ -593,8 +596,7 @@
         return navigator
 
 
-# A simple vocabulary for package filtering on the source package
-# differences page
+# A helper to create package filtering radio button vocabulary.
 NON_BLACKLISTED = 'non-blacklisted'
 BLACKLISTED = 'blacklisted'
 HIGHER_VERSION_THAN_PARENT = 'higher-than-parent'
@@ -603,20 +605,20 @@
 DEFAULT_PACKAGE_TYPE = NON_BLACKLISTED
 
 
-def make_package_type_vocabulary(parent_name):
-    return SimpleVocabulary((
+def make_package_type_vocabulary(parent_name, higher_version_option=False):
+    voc = [
         SimpleTerm(
             NON_BLACKLISTED, NON_BLACKLISTED, 'Non blacklisted packages'),
         SimpleTerm(BLACKLISTED, BLACKLISTED, 'Blacklisted packages'),
-        SimpleTerm(
+        SimpleTerm(RESOLVED, RESOLVED, "Resolved packages")]
+    if higher_version_option:
+        higher_term = SimpleTerm(
             HIGHER_VERSION_THAN_PARENT,
             HIGHER_VERSION_THAN_PARENT,
             "Blacklisted packages with a higher version than in '%s'"
-                % parent_name),
-        SimpleTerm(
-            RESOLVED,
-            RESOLVED,
-            "Resolved packages")))
+                % parent_name)
+        voc.insert(2, higher_term)
+    return SimpleVocabulary(tuple(voc))
 
 
 class DistroSeriesNeedsPackagesView(LaunchpadView):
@@ -645,14 +647,25 @@
         required=True)
 
 
-class DistroSeriesLocalDifferences(LaunchpadFormView, PackageCopyingMixin):
-    """Present differences between a derived series and its parent."""
+class DistroSeriesDifferenceBase(LaunchpadFormView,
+                                 PackageCopyingMixin):
+    """Base class for all pages presenting differences between
+    a derived series and its parent."""
     schema = IDifferencesFormSchema
     field_names = ['selected_differences']
     custom_widget('selected_differences', LabeledMultiCheckBoxWidget)
     custom_widget('package_type', LaunchpadRadioWidget)
 
-    page_title = 'Local package differences'
+    # Differences type to display. Can be overrided by sublasses.
+    differences_type = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
+    show_parent_version = True
+    show_derived_version = True
+    show_package_diffs = True
+    # Packagesets display.
+    show_parent_packagesets = False
+    show_packagesets = False
+    # Search vocabulary.
+    search_higher_parent_option = False
 
     def initialize(self):
         """Redirect to the derived series if the feature is not enabled."""
@@ -660,29 +673,21 @@
             self.request.response.redirect(canonical_url(self.context))
             return
 
-        # Update the label for sync action.
-        self.__class__.actions.byname['actions.sync'].label = (
-            "Sync Selected %s Versions into %s" % (
-                self.context.parent_series.displayname,
-                self.context.displayname,
-                ))
+        super(DistroSeriesDifferenceBase, self).initialize()
 
-        super(DistroSeriesLocalDifferences, self).initialize()
+    def initialize_sync_label(self, label):
+        self.__class__.actions.byname['actions.sync'].label = label
 
     @property
     def label(self):
-        return (
-            "Source package differences between '%s' and "
-            "parent series '%s'" % (
-                self.context.displayname,
-                self.context.parent_series.displayname,
-                ))
+        return NotImplementedError()
 
     def setupPackageFilterRadio(self):
         return form.Fields(Choice(
             __name__='package_type',
             vocabulary=make_package_type_vocabulary(
-                self.context.parent_series.displayname),
+                self.context.parent_series.displayname,
+                self.search_higher_parent_option),
             default=DEFAULT_PACKAGE_TYPE,
             required=True))
 
@@ -692,7 +697,7 @@
         As this field depends on other search/filtering field values
         for its own vocabulary, we set it up after all the others.
         """
-        super(DistroSeriesLocalDifferences, self).setUpFields()
+        super(DistroSeriesDifferenceBase, self).setUpFields()
         self.form_fields = (
             self.setupPackageFilterRadio() +
             self.form_fields)
@@ -705,14 +710,7 @@
         choice = self.form_fields['selected_differences'].field.value_type
         choice.vocabulary = diffs_vocabulary
 
-    @action(_("Update"), name="update")
-    def update_action(self, action, data):
-        """Simply re-issue the form with the new values."""
-        pass
-
-    @action(_("Sync Sources"), name="sync", validator='validate_sync',
-            condition='canPerformSync')
-    def sync_sources(self, action, data):
+    def _sync_sources(self, action, data):
         """Synchronise packages from the parent series to this one."""
         # We're doing a direct copy sync here as an interim measure
         # until we work out if it's fast enough to work reliably.  If it
@@ -751,7 +749,8 @@
         This method is used as a condition for the above sync action, as
         well as directly in the template.
         """
-        return check_permission('launchpad.Edit', self.context)
+        return (check_permission('launchpad.Edit', self.context) and
+                self.cached_differences.batch.total() > 0)
 
     @property
     def specified_name_filter(self):
@@ -800,6 +799,7 @@
         differences = getUtility(
             IDistroSeriesDifferenceSource).getForDistroSeries(
                 self.context,
+                difference_type = self.differences_type,
                 source_package_name_filter=self.specified_name_filter,
                 status=status,
                 child_version_higher=child_version_higher)
@@ -820,7 +820,110 @@
             differences = getUtility(
                 IDistroSeriesDifferenceSource).getForDistroSeries(
                     self.context,
+                    difference_type = self.differences_type,
                     status=(
                         DistroSeriesDifferenceStatus.NEEDS_ATTENTION,
                         DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT))
             return not differences.is_empty()
+
+
+class DistroSeriesLocalDifferences(DistroSeriesDifferenceBase,
+                                   LaunchpadFormView):
+    """Present differences between a derived series and its parent."""
+    page_title = 'Local package differences'
+    differences_type = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
+    show_parent_packagesets = True
+    search_higher_parent_option = True
+
+    def initialize(self):
+        # Update the label for sync action.
+        self.initialize_sync_label(
+            "Sync Selected %s Versions into %s" % (
+                self.context.parent_series.displayname,
+                self.context.displayname,
+                ))
+        super(DistroSeriesLocalDifferences, self).initialize()
+
+    @property
+    def explanation(self):
+        return (
+            "Source packages shown here are present in both %s "
+            "and the parent series, %s, but are different somehow. "
+            "Changes could be in either or both series so check the "
+            "versions (and the diff if necessary) before syncing the %s "
+            'version (<a href="/+help/soyuz/derived-series-syncing.html" '
+            'target="help">Read more about syncing from the parent series'
+            '</a>).' % (
+                self.context.displayname,
+                self.context.parent_series.fullseriesname,
+                self.context.parent_series.displayname,
+                ))
+
+    @property
+    def label(self):
+        return (
+            "Source package differences between '%s' and "
+            "parent series '%s'" % (
+                self.context.displayname,
+                self.context.parent_series.displayname,
+                ))
+
+    @property
+    def has_available_actions(self):
+        return False#self.has_differences
+
+    @action(_("Update"), name="update")
+    def update_action(self, action, data):
+        """Simply re-issue the form with the new values."""
+        pass
+
+    @action(_("Sync Sources"), name="sync", validator='validate_sync',
+            condition='canPerformSync')
+    def sync_sources(self, action, data):
+        self._sync_sources(action, data)
+
+
+class DistroSeriesMissingPackages(DistroSeriesDifferenceBase,
+                                   LaunchpadFormView):
+    page_title = 'Missing packages'
+    differences_type = DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES
+    show_derived_version = False
+    show_package_diffs = False
+    show_parent_packagesets = True
+
+    def initialize(self):
+        # Update the label for sync action.
+        self.initialize_sync_label(
+            "Include Selected packages into into %s" % (
+                self.context.displayname,
+                ))
+        super(DistroSeriesMissingPackages, self).initialize()
+
+    @property
+    def explanation(self):
+        return (
+            "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.context.displayname,
+                self.context.displayname,
+                ))
+
+    @property
+    def label(self):
+        return (
+            "Packages in parent series '%s' but not in '%s'" % (
+                self.context.parent_series.displayname,
+                self.context.displayname,
+                ))
+
+    @action(_("Update"), name="update")
+    def update_action(self, action, data):
+        """Simply re-issue the form with the new values."""
+        pass
+
+    @action(_("Sync Sources"), name="sync", validator='validate_sync',
+            condition='canPerformSync')
+    def sync_sources(self, action, data):
+        self._sync_sources(action, data)

=== modified file 'lib/lp/registry/browser/distroseriesdifference.py'
--- lib/lp/registry/browser/distroseriesdifference.py	2011-04-01 14:16:37 +0000
+++ lib/lp/registry/browser/distroseriesdifference.py	2011-04-04 11:43:30 +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).
 
 """Browser views for DistroSeriesDifferences."""
@@ -36,7 +36,10 @@
 from canonical.launchpad.webapp.authorization import check_permission
 from canonical.launchpad.webapp.launchpadform import custom_widget
 from lp.app.browser.launchpadform import LaunchpadFormView
-from lp.registry.enum import DistroSeriesDifferenceStatus
+from lp.registry.enum import (
+    DistroSeriesDifferenceStatus,
+    DistroSeriesDifferenceType,
+    )
 from lp.registry.interfaces.distroseriesdifference import (
     IDistroSeriesDifference,
     )
@@ -100,6 +103,29 @@
                 DistroSeriesSourcePackageRelease(
                     distro_series, pub.sourcepackagerelease))
 
+    @property
+    def parent_packageset_names(self):
+        """Return the formatted list of packagesets for the related
+        sourcepackagename in the parent.
+        """
+        packagesets = self.context.getParentPackageSets()
+        return self._format_packageset(packagesets)
+
+    @property
+    def packageset_names(self):
+        """Return the formatted list of packagesets for the related
+        sourcepackagename in the derived series.
+        """
+        packagesets = self.context.getPackageSets()
+        return self._format_packageset(packagesets)
+
+    def _format_packageset(self, packagesets):
+        """Format a list of packagesets to display in the UI."""
+        if packagesets is not None:
+            return ', '.join([p.name for p in packagesets])
+        else:
+            return None
+
 
 class IDistroSeriesDifferenceForm(Interface):
     """An interface used in the browser only for displaying form elements."""
@@ -169,7 +195,13 @@
     def display_child_diff(self):
         """Only show the child diff if we need to."""
         return self.context.source_version != self.context.base_version
-  
+
+    @property
+    def can_have_packages_diffs(self):
+        """Return whether this dsd could have packages diffs."""
+        diff_versions = DistroSeriesDifferenceType.DIFFERENT_VERSIONS
+        return self.context.difference_type == diff_versions
+
     @property
     def show_package_diffs_request_link(self):
         """Return whether package diffs can be requested.

=== modified file 'lib/lp/registry/browser/tests/test_distroseriesdifference_views.py'
--- lib/lp/registry/browser/tests/test_distroseriesdifference_views.py	2011-03-31 16:21:15 +0000
+++ lib/lp/registry/browser/tests/test_distroseriesdifference_views.py	2011-04-04 11:43:30 +0000
@@ -15,10 +15,7 @@
 
 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
 from canonical.launchpad.webapp.testing import verifyObject
-from canonical.testing import (
-    DatabaseFunctionalLayer,
-    LaunchpadFunctionalLayer,
-    )
+from canonical.testing import LaunchpadFunctionalLayer
 from lp.registry.browser.distroseriesdifference import (
     DistroSeriesDifferenceDisplayComment,
     )
@@ -208,6 +205,29 @@
         tags = soup.find('ul', 'package-diff-status').findAll('span')
         self.assertEqual(1, len(tags))
 
+    def test_packagediffs_display(self):
+        # The packages diffs slots are displayed only when the diff
+        # is of type DIFFERENT_VERSIONS.
+        pck_diff_type = (
+            DistroSeriesDifferenceType.DIFFERENT_VERSIONS,)
+        non_pck_diff_type = (
+            DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES,
+            DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES)
+
+        for ptype in pck_diff_type:
+            ds_diff = self.factory.makeDistroSeriesDifference(
+                difference_type=ptype)
+            view = create_initialized_view(
+                ds_diff, '+listing-distroseries-extra')
+            self.assertTrue(view.can_have_packages_diffs)
+
+        for ptype in non_pck_diff_type:
+            ds_diff = self.factory.makeDistroSeriesDifference(
+                difference_type=ptype)
+            view = create_initialized_view(
+                ds_diff, '+listing-distroseries-extra')
+            self.assertFalse(view.can_have_packages_diffs)
+
 
 class DistroSeriesDifferenceTemplateTestCase(TestCaseWithFactory):
 
@@ -310,7 +330,7 @@
                 (DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES))
 
         view = create_initialized_view(ds_diff, '+listing-distroseries-extra')
-        self.assertEqual(1, self.number_of_request_diff_texts(view()))
+        self.assertEqual(0, self.number_of_request_diff_texts(view()))
 
     def test_parent_source_diff_rendering_diff(self):
         # A linked description of the diff is displayed when
@@ -337,7 +357,7 @@
                 (DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES))
 
         view = create_initialized_view(ds_diff, '+listing-distroseries-extra')
-        self.assertEqual(1, self.number_of_request_diff_texts(view()))
+        self.assertEqual(0, self.number_of_request_diff_texts(view()))
 
     def test_comments_rendered(self):
         # If there are comments on the difference, they are rendered.

=== modified file 'lib/lp/registry/browser/tests/test_series_views.py'
--- lib/lp/registry/browser/tests/test_series_views.py	2011-04-04 01:42:16 +0000
+++ lib/lp/registry/browser/tests/test_series_views.py	2011-04-04 11:43:30 +0000
@@ -3,8 +3,6 @@
 
 __metaclass__ = type
 
-import unittest
-
 from BeautifulSoup import BeautifulSoup
 import soupmatchers
 from storm.zope.interfaces import IResultSet
@@ -49,6 +47,7 @@
     ISourcePackageFormatSelectionSet,
     )
 from lp.testing import (
+    celebrity_logged_in,
     login_person,
     person_logged_in,
     TestCaseWithFactory,
@@ -145,6 +144,32 @@
             find_tag_by_id(view(), 'distroseries-localdiff-search-filter'),
             "Form filter should not be shown when there are no differences.")
 
+    def test_parent_packagesets_localpackagediffs(self):
+        # +localpackagediffs displays the parent packagesets.
+        pckset_names = [u'pack2', u'pack1', u'aa']
+        sorted_pckset_names = [u'aa', u'pack1', u'pack2']
+        ds_diff = self.factory.makeDistroSeriesDifference()
+        with celebrity_logged_in('admin'):
+            for pckset_name in pckset_names:
+                ps = self.factory.makePackageset(
+                    name=pckset_name,
+                    packages=[ds_diff.source_package_name],
+                    distroseries=ds_diff.derived_series.parent_series)
+
+        with person_logged_in(self.simple_user):
+            view = create_initialized_view(
+                ds_diff.derived_series,
+                '+localpackagediffs',
+                principal=self.simple_user)
+            html = view()
+
+        parent_packagesets = soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                'Parent packagesets', 'td',
+                attrs={'class': 'parent-packagesets'}))
+
+        self.assertThat(html, parent_packagesets)
+
 
 class DistroSeriesLocalPackageDiffsTestCase(TestCaseWithFactory):
     """Test the distroseries +localpackagediffs view."""
@@ -371,7 +396,6 @@
 
     layer = LaunchpadFunctionalLayer
 
-
     def test_higher_radio_mentions_parent(self):
         set_derived_series_ui_feature_flag(self)
         parent_series = self.factory.makeDistroSeries(
@@ -669,6 +693,76 @@
             'Invalid value', view.errors[1].error_name)
 
 
+class DistroSerieMissingPackageDiffsTestCase(TestCaseWithFactory):
+    """Test the distroseries +missingpackages view."""
+
+    layer = LaunchpadZopelessLayer
+
+    def test_missingpackages_differences(self):
+        # The view fetches the differences with type
+        # MISSING_FROM_DERIVED_SERIES.
+        derived_series = self.factory.makeDistroSeries(
+            name='derilucid', parent_series=self.factory.makeDistroSeries(
+                name='lucid'))
+
+        missing_type = DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES
+        missing_blacklisted_diff = self.factory.makeDistroSeriesDifference(
+            difference_type=missing_type,
+            derived_series=derived_series,
+            status=DistroSeriesDifferenceStatus.BLACKLISTED_CURRENT)
+
+        missing_diff = self.factory.makeDistroSeriesDifference(
+            difference_type=missing_type,
+            derived_series=derived_series,
+            status=DistroSeriesDifferenceStatus.NEEDS_ATTENTION)
+
+        view = create_initialized_view(
+            derived_series, '+missingpackages')
+
+        self.assertContentEqual(
+            [missing_diff], view.cached_differences.batch)
+
+
+class DistroSeriesMissingPackagesPageTestCase(TestCaseWithFactory):
+    """Test the distroseries +missingpackages page."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(DistroSeriesMissingPackagesPageTestCase,
+              self).setUp('foo.bar@xxxxxxxxxxxxx')
+        set_derived_series_ui_feature_flag(self)
+        self.simple_user = self.factory.makePerson()
+        missing_type = DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES
+        self.ds_diff = self.factory.makeDistroSeriesDifference(
+            difference_type=missing_type)
+
+    def test_parent_packagesets_missingpackages(self):
+        # +missingpackages displays the packagesets in the parent.
+        pckset_names = [u'pack2', u'pack1', u'aa']
+        sorted_pckset_names = [u'aa', u'pack1', u'pack2']
+        with celebrity_logged_in('admin'):
+            for pckset_name in pckset_names:
+                ps = self.factory.makePackageset(
+                    name=pckset_name,
+                    packages=[self.ds_diff.source_package_name],
+                    distroseries=self.ds_diff.derived_series.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()
+
+        parent_packagesets = soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                'Packagesets', 'td',
+                attrs={'class': 'parent-packagesets'}))
+
+        self.assertThat(html, parent_packagesets)
+
+
 class TestMilestoneBatchNavigatorAttribute(TestCaseWithFactory):
     """Test the series.milestone_batch_navigator attribute."""
 

=== modified file 'lib/lp/registry/interfaces/distroseriesdifference.py'
--- lib/lp/registry/interfaces/distroseriesdifference.py	2011-04-04 01:42:16 +0000
+++ lib/lp/registry/interfaces/distroseriesdifference.py	2011-04-04 11:43:30 +0000
@@ -175,6 +175,16 @@
     def getComments():
         """Return a result set of the comments for this difference."""
 
+    def getPackageSets():
+        """Return a result set of the derived series packagesets for the
+        sourcepackagename of this difference.
+        """
+
+    def getParentPackageSets():
+        """Return a result set of the parent packagesets for the
+        sourcepackagename of this difference.
+        """
+
 
 class IDistroSeriesDifferenceEdit(Interface):
     """Difference attributes requiring launchpad.Edit."""

=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
--- lib/lp/registry/model/distroseriesdifference.py	2011-04-04 01:45:43 +0000
+++ lib/lp/registry/model/distroseriesdifference.py	2011-04-04 11:43:30 +0000
@@ -57,6 +57,7 @@
     clear_property_cache,
     )
 from lp.soyuz.enums import PackageDiffStatus
+from lp.soyuz.interfaces.packageset import IPackagesetSet
 
 
 class DistroSeriesDifference(Storm):
@@ -241,6 +242,24 @@
         """See `IDistroSeriesDifference`."""
         return self._getPackageDiffURL(self.parent_package_diff)
 
+    def getPackageSets(self):
+        """See `IDistroSeriesDifference`."""
+        if self.derived_series is not None:
+            return getUtility(IPackagesetSet).setsIncludingSource(
+                self.source_package_name, self.derived_series)
+        else:
+            return []
+
+    def getParentPackageSets(self):
+        """See `IDistroSeriesDifference`."""
+        if self.derived_series is not None and (
+            self.derived_series.parent_series is not None):
+                return getUtility(IPackagesetSet).setsIncludingSource(
+                    self.source_package_name,
+                    self.derived_series.parent_series)
+        else:
+            return []
+
     @property
     def package_diff_status(self):
         """See `IDistroSeriesDifference`."""

=== modified file 'lib/lp/registry/templates/distroseries-localdifferences.pt'
--- lib/lp/registry/templates/distroseries-localdifferences.pt	2011-04-04 01:42:16 +0000
+++ lib/lp/registry/templates/distroseries-localdifferences.pt	2011-04-04 11:43:30 +0000
@@ -19,22 +19,12 @@
       <h1 tal:content="view/label">Package differences between ...</h1>
     </tal:heading>
 
+
     <div class="top-portlet" metal:fill-slot="main"
       tal:define="differences view/cached_differences;
                   series_name context/displayname;
                   parent_name context/parent_series/displayname;">
-      <p>Source packages shown here are present in both
-         <tal:replace replace="series_name">Derilucid</tal:replace>
-         and the parent series,
-         <tal:replace replace="context/parent_series/fullseriesname">Ubuntu Lucid
-         </tal:replace>, but are different somehow. Changes could be in
-         either or both series so check the versions (and the diff if
-         necessary) before syncing the
-         <tal:replace replace="parent_name">Lucid
-         </tal:replace> version
-         (<a href="/+help/soyuz/derived-series-syncing.html" target="help">Read
-             more about syncing from the parent series</a>).
-      </p>
+      <p><tal:replace replace="structure view/explanation" /></p>
 
       <tal:distroseries_localdiff_search_form
         tal:condition="view/has_differences">
@@ -52,9 +42,17 @@
           <thead>
             <tr>
               <th>Source</th>
-              <th><tal:replace replace="parent_name" /> version</th>
-              <th><tal:replace replace="series_name" /> version</th>
-              <th>Last uploaded</th>
+              <th tal:condition="view/show_parent_version">
+                <tal:replace replace="parent_name" /> version</th>
+              <th tal:condition="view/show_derived_version">
+                <tal:replace replace="series_name" /> version</th>
+              <th tal:condition="view/show_parent_packagesets">
+                Parent package-sets
+              </th>
+              <th tal:condition="view/show_packagesets">
+                Package-sets
+              </th>
+               <th>Last uploaded</th>
               <th>Latest comment</th>
             </tr>
           </thead>
@@ -62,8 +60,7 @@
             <tal:difference repeat="difference differences/batch">
             <tr tal:define="parent_source_pub difference/parent_source_pub;
                             source_pub difference/source_pub;
-                            src_name difference/source_package_name/name;
-                            signer source_pub/sourcepackagerelease/uploader;"
+                            src_name difference/source_package_name/name;"
                 tal:attributes="class parent_source_pub/source_package_name">
               <td>
                 <input tal:condition="view/canPerformSync"
@@ -73,36 +70,53 @@
                     id string:field.selected_differences.${src_name}"/>
 
                 <a tal:attributes="href difference/fmt:url" class="toggle-extra"
-                    tal:content="parent_source_pub/source_package_name">Foo</a>
+                   tal:content="parent_source_pub/source_package_name">Foo</a>
               </td>
-              <td tal:define="parent_source_pck_url difference/@@/parent_source_package_url">
-                <a tal:condition="parent_source_pck_url"
-                    tal:attributes="href difference/@@/parent_source_package_url"
-                    class="parent-version">
-                    <tal:replace
+              <td tal:condition="view/show_parent_version">
+                <a tal:condition="difference/@@/parent_source_package_url"
+                   tal:attributes="href difference/@@/parent_source_package_url"
+                   class="parent-version">
+                   <tal:replace
                         replace="difference/parent_source_version"/></a>
-                <span tal:condition="not: parent_source_pck_url"
-                    class="parent-version"
-                    tal:content="difference/parent_source_version">
+                <span tal:condition="not: difference/@@/parent_source_package_url"
+                      class="parent-version"
+                      tal:content="difference/parent_source_version">
                 </span>
               </td>
-              <td tal:define="source_pck_url difference/@@/source_package_url">
-                <a tal:condition="source_pck_url"
-                    tal:attributes="href difference/@@/source_package_url"
-                    class="derived-version">
-                    <tal:replace
+              <td tal:condition="view/show_derived_version">
+                <a tal:condition="difference/@@/source_package_url"
+                   tal:attributes="href difference/@@/source_package_url"
+                   class="derived-version">
+                   <tal:replace
                         replace="difference/source_version"/></a>
-                <span tal:condition="not: source_pck_url"
+                <span tal:condition="not: difference/@@/source_package_url"
                     class="derived-version"
                     tal:content="difference/source_version">
                 </span>
                </td>
+              <td tal:condition="view/show_parent_packagesets"
+                  class="parent-packagesets">
+                <tal:replace replace="difference/@@/parent_packageset_names"/>
+              </td>
+              <td tal:condition="view/show_packagesets"
+                  class="packagesets">
+                <tal:replace replace="difference/@@/packageset_names" />
+              </td>
               <td>
-                  <span tal:attributes="title difference/source_pub/datepublished/fmt:datetime"
-                        tal:content="difference/source_pub/datepublished/fmt:approximatedate">2005-09-16</span>
-                  <tal:signer condition="signer">
-                   by <a tal:replace="structure signer/fmt:link">Steph Smith</a>
-                  </tal:signer>
+                  <tal:parent condition="not: view/show_derived_version">
+                    <span tal:attributes="title difference/parent_source_pub/datepublished/fmt:datetime"
+                          tal:content="difference/parent_source_pub/datepublished/fmt:approximatedate">2005-09-16</span>
+                    <tal:signer condition="parent_source_pub/sourcepackagerelease/uploader">
+                      by <a tal:replace="structure parent_source_pub/sourcepackagerelease/uploader/fmt:link">Steph Smith</a>
+                    </tal:signer>
+                  </tal:parent>
+                  <tal:derived condition="view/show_derived_version">
+                    <span tal:attributes="title difference/parent_source_pub/datepublished/fmt:datetime"
+                          tal:content="difference/parent_source_pub/datepublished/fmt:approximatedate">2005-09-16</span>
+                    <tal:signer condition="source_pub/sourcepackagerelease/uploader">
+                      by <a tal:replace="structure source_pub/sourcepackagerelease/uploader/fmt:link">Steph Smith</a>
+                    </tal:signer>
+                  </tal:derived>
               </td>
               <td>
                 <tal:comment tal:define="comment python:difference.getComments().first();"

=== modified file 'lib/lp/registry/templates/distroseriesdifference-listing-extra.pt'
--- lib/lp/registry/templates/distroseriesdifference-listing-extra.pt	2011-03-31 20:40:32 +0000
+++ lib/lp/registry/templates/distroseriesdifference-listing-extra.pt	2011-04-04 11:43:30 +0000
@@ -37,6 +37,7 @@
         <li tal:repeat="summary view/binary_summaries">
           <tal:description replace="summary" /></li>
       </ul></dd>
+    <tal:package-diffs tal:condition="view/can_have_packages_diffs">
     <dt>Last common version:</dt>
     <dd tal:content="context/base_version">1.2.1</dd>
     <dt
@@ -111,6 +112,7 @@
 
       </ul>
     </dd>
+    </tal:package-diffs>
   </dl>
 
   <h2>Comments:</h2>

=== modified file 'lib/lp/registry/tests/test_distroseriesdifference.py'
--- lib/lp/registry/tests/test_distroseriesdifference.py	2011-04-04 01:45:43 +0000
+++ lib/lp/registry/tests/test_distroseriesdifference.py	2011-04-04 11:43:30 +0000
@@ -31,6 +31,7 @@
 from lp.soyuz.enums import PackageDiffStatus
 from lp.soyuz.interfaces.publishing import PackagePublishingStatus
 from lp.testing import (
+    celebrity_logged_in,
     person_logged_in,
     TestCaseWithFactory,
     )
@@ -337,6 +338,46 @@
             diff_comment = ds_diff.addComment(
                 ds_diff.derived_series.owner, "Boo")
 
+    def test_getParentPackageSets(self):
+        # All parent's packagesets are returned ordered alphabetically.
+        ds_diff = self.factory.makeDistroSeriesDifference()
+
+        self.factory.makePackageset()
+        self.factory.makePackageset(
+            packages=[ds_diff.source_package_name],
+            distroseries=ds_diff.derived_series)
+        pckset_names = [u'pack2', u'pack1', u'aa']
+        sorted_pckset_names = [u'aa', u'pack1', u'pack2']
+        with celebrity_logged_in('admin'):
+            for pckset_name in pckset_names:
+                ps = self.factory.makePackageset(
+                    name=pckset_name,
+                    packages=[ds_diff.source_package_name],
+                    distroseries=ds_diff.derived_series.parent_series)
+        self.assertEquals(
+            sorted_pckset_names,
+            [ps.name for ps in ds_diff.getParentPackageSets()])
+
+    def test_getPackageSets(self):
+        # All the packagesets are returned ordered alphabetically.
+        ds_diff = self.factory.makeDistroSeriesDifference()
+
+        self.factory.makePackageset()
+        self.factory.makePackageset(
+            packages=[ds_diff.source_package_name],
+            distroseries=ds_diff.derived_series.parent_series)
+        pckset_names = [u'pack2', u'pack1', u'aa']
+        sorted_pckset_names = [u'aa', u'pack1', u'pack2']
+        with celebrity_logged_in('admin'):
+            for pckset_name in pckset_names:
+                ps = self.factory.makePackageset(
+                    name=pckset_name,
+                    packages=[ds_diff.source_package_name],
+                    distroseries=ds_diff.derived_series)
+        self.assertEquals(
+            sorted_pckset_names,
+            [ps.name for ps in ds_diff.getPackageSets()])
+
     def test_blacklist_not_public(self):
         # Differences cannot be blacklisted without edit access.
         ds_diff = self.factory.makeDistroSeriesDifference()