← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/launchpad/bug-798521 into lp:launchpad

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/bug-798521 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #798521 in Launchpad itself: "distroseries-queue.pt logic should be in view"
  https://bugs.launchpad.net/launchpad/+bug/798521

For more details, see:
https://code.launchpad.net/~jtv/launchpad/bug-798521/+merge/65297

= Summary =

This cleans up some technical debt related to the +queue page, in penance for my first adding to it in a hurry.


== Implementation details ==

Here's a quick run-through of what I changed (a bit of a grab-bag, but it's all connected):

Added a Storm class for the PackagesetSources linking table, which was previously hacked into the python ad-hoc.

Batch-fetched any SourcePackageReleases associated with the PackageUploads on the page.

Batch-fetched any SourcePackageNames associated with the SourcePackageReleases.

Batch-fetched any Packagesets associated with the SourcePackages.

Extended CompletePackageUpload to be less like a "PackageUpload with some data pre-fetched" and more like a "view class for PackageUploads."

Moved some small-scale TAL logic into CompletePackageUpload (but not so much as to split the <table> structure across TAL and python).

Unit-tested CompletePackageUpload.

Modernized the PackagesetSet unit test, replacing its setUp with specific factory calls.


== Tests ==

{{{
./bin/test -vvc soyuz.browser.tests.test_queue
./bin/test -vvc soyuz.tests.test_packageset
./bin/test -vvc soyuz -t stories
}}}


== Demo and Q/A ==

The +queue page should still work, and be no less efficient.  (Hopefully it will be slightly more efficient).


= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/browser/tests/test_queue.py
  lib/lp/soyuz/templates/distroseries-queue.pt
  lib/lp/soyuz/model/packagesetsources.py
  lib/lp/soyuz/browser/queue.py
  lib/lp/soyuz/interfaces/packageset.py
  lib/lp/soyuz/model/packageset.py
  lib/lp/soyuz/tests/test_packageset.py
  lib/lp/registry/model/distroseriesdifference.py
-- 
https://code.launchpad.net/~jtv/launchpad/bug-798521/+merge/65297
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/bug-798521 into lp:launchpad.
=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
--- lib/lp/registry/model/distroseriesdifference.py	2011-06-14 13:47:51 +0000
+++ lib/lp/registry/model/distroseriesdifference.py	2011-06-21 05:48:36 +0000
@@ -97,6 +97,7 @@
     DistroSeriesSourcePackageRelease,
     )
 from lp.soyuz.model.packageset import Packageset
+from lp.soyuz.model.packagesetsources import PackagesetSources
 from lp.soyuz.model.publishing import SourcePackagePublishingHistory
 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
 
@@ -205,7 +206,6 @@
     if len(dsds) == 0:
         return {}
 
-    PackagesetSources = Table("PackageSetSources")
     FlatPackagesetInclusion = Table("FlatPackagesetInclusion")
 
     tables = IStore(Packageset).using(
@@ -213,18 +213,17 @@
         PackagesetSources, FlatPackagesetInclusion)
     results = tables.find(
         (DistroSeriesDifference.id, Packageset),
-        Column("packageset", PackagesetSources) == (
-            Column("child", FlatPackagesetInclusion)),
+        PackagesetSources.packageset == Column(
+            "child", FlatPackagesetInclusion),
         Packageset.distroseries_id == (
             DistroSeriesDifference.parent_series_id if in_parent else
             DistroSeriesDifference.derived_series_id),
         Column("parent", FlatPackagesetInclusion) == Packageset.id,
-        Column("sourcepackagename", PackagesetSources) == (
+        PackagesetSources.sourcepackagename == (
             DistroSeriesDifference.source_package_name_id),
         DistroSeriesDifference.id.is_in(dsd.id for dsd in dsds))
     results = results.order_by(
-        Column("sourcepackagename", PackagesetSources),
-        Packageset.name)
+        PackagesetSources.sourcepackagename, Packageset.name)
 
     grouped = defaultdict(list)
     for dsd_id, packageset in results:

=== modified file 'lib/lp/soyuz/browser/queue.py'
--- lib/lp/soyuz/browser/queue.py	2011-06-16 10:44:00 +0000
+++ lib/lp/soyuz/browser/queue.py	2011-06-21 05:48:36 +0000
@@ -9,6 +9,7 @@
     'QueueItemsView',
     ]
 
+import cgi
 import operator
 
 from lazr.delegates import delegates
@@ -22,6 +23,7 @@
     NotFoundError,
     UnexpectedFormData,
     )
+from lp.services.database.bulk import load_related
 from lp.soyuz.enums import (
     PackagePublishingPriority,
     PackageUploadStatus,
@@ -176,6 +178,18 @@
         # Listify to avoid repeated queries.
         return list(old_binary_packages)
 
+    def getPackagesetsForSourceFiles(self, source_files):
+        """Get the `Packagesets` belonging to `source_files`.
+
+        :param source_files: A sequence of `SourcePackageReleaseFile`s.
+        """
+        sprs = set(
+            source_file.sourcepackagerelease for source_file in source_files)
+        if None in sprs:
+            sprs.remove(None)
+        return getUtility(IPackagesetSet).getForPackages(
+            self.context, [spr.sourcepackagename_id for spr in sprs])
+
     def decoratedQueueBatch(self):
         """Return the current batch, converted to decorated objects.
 
@@ -183,6 +197,9 @@
         CompletePackageUpload.  This avoids many additional SQL queries
         in the +queue template.
         """
+        # Avoid circular imports.
+        from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
+
         uploads = list(self.batchnav.currentBatch())
 
         if len(uploads) == 0:
@@ -199,6 +216,9 @@
         source_file_set = getUtility(ISourcePackageReleaseFileSet)
         source_files = source_file_set.getByPackageUploadIDs(upload_ids)
 
+        load_related(
+            SourcePackageRelease, source_files, ['sourcepackagerelease_id'])
+
         # Get a dictionary of lists of binary files keyed by upload ID.
         package_upload_builds_dict = self.builds_dict(
             upload_ids, binary_files)
@@ -217,9 +237,11 @@
         self.old_binary_packages = self.calculateOldBinaries(
             binary_package_names)
 
+        package_sets = self.getPackagesetsForSourceFiles(source_files)
+
         return [
             CompletePackageUpload(
-                item, build_upload_files, source_upload_files)
+                item, build_upload_files, source_upload_files, package_sets)
             for item in uploads]
 
     def is_new(self, binarypackagerelease):
@@ -415,8 +437,9 @@
 class CompletePackageUpload:
     """A decorated `PackageUpload` including sources, builds and packages.
 
-    Some properties of PackageUpload are cached here to reduce the number
-    of queries that the +queue template has to make.
+    This acts effectively as a view for package uploads.  Some properties of
+    the class are cached here to reduce the number of queries that the +queue
+    template has to make.  Others are added here exclusively.
     """
     # These need to be predeclared to avoid delegates taking them over.
     # Would be nice if there was a way of allowing writes to just work
@@ -433,7 +456,7 @@
     delegates(IPackageUpload)
 
     def __init__(self, packageupload, build_upload_files,
-                 source_upload_files):
+                 source_upload_files, package_sets):
         self.pocket = packageupload.pocket
         self.date_created = packageupload.date_created
         self.context = packageupload
@@ -459,6 +482,12 @@
         else:
             self.sourcepackagerelease = None
 
+        if self.contains_source:
+            self.package_sets = package_sets.get(
+                self.sourcepackagerelease.sourcepackagenameID, [])
+        else:
+            self.package_sets = []
+
     @property
     def pending_delayed_copy(self):
         """Whether the context is a delayed-copy pending processing."""
@@ -478,11 +507,87 @@
         return self.context.changesfile
 
     @property
-    def package_sets(self):
-        assert self.sourcepackagerelease, \
-            "Can only be used on a source upload."
-        return ' '.join(sorted(ps.name for ps in
-            getUtility(IPackagesetSet).setsIncludingSource(
-                self.sourcepackagerelease.sourcepackagename,
-                distroseries=self.distroseries,
-                direct_inclusion=True)))
+    def display_package_sets(self):
+        """Package sets, if any, for display on the +queue page."""
+        if self.contains_source:
+            return ' '.join(sorted(
+                packageset.name for packageset in self.package_sets))
+        else:
+            return ""
+
+    @property
+    def display_component(self):
+        """Component name, if any, for display on the +queue page."""
+        if self.contains_source:
+            return self.component_name.lower()
+        else:
+            return ""
+
+    @property
+    def display_section(self):
+        """Section name, if any, for display on the +queue page."""
+        if self.contains_source:
+            return self.section_name.lower()
+        else:
+            return ""
+
+    @property
+    def display_priority(self):
+        """Priority name, if any, for display on the +queue page."""
+        if self.contains_source:
+            return self.sourcepackagerelease.urgency.name.lower()
+        else:
+            return ""
+
+    def composeIcon(self, alt, icon, title=None):
+        """Compose an icon for the package's icon list."""
+        # These should really be sprites!
+        if title is None:
+            title = alt
+        return '<img alt="[%s]" src="/@@/%s" title="%s" />' % (
+            cgi.escape(alt, quote=True),
+            icon,
+            cgi.escape(title, quote=True),
+            )
+
+    def composeIconList(self):
+        """List icons that should be shown for this upload."""
+        ddtp = "Debian Description Translation Project Indexes"
+        potential_icons = [
+            (self.contains_source, ("Source", 'package-source')),
+            (self.contains_build, ("Build", 'package-binary', "Binary")),
+            (self.contains_translation, ("Translation", 'translation-file')),
+            (self.contains_installer, ("Installer", 'ubuntu-icon')),
+            (self.contains_upgrader, ("Upgrader", 'ubuntu-icon')),
+            (self.contains_ddtp, (ddtp, 'ubuntu-icon')),
+            ]
+        return [
+            self.composeIcon(*details)
+            for condition, details in potential_icons
+                if condition]
+
+    def composeNameAndChangesLink(self):
+        """Compose HTML: upload name and link to changes file."""
+        raw_displayname = self.displayname
+        displayname = cgi.escape(raw_displayname)
+        if self.pending_delayed_copy or self.changesfile is None:
+            return displayname
+        else:
+            return '<a href="%s" title="Changes file for %s">%s</a>' % (
+                self.changesfile.http_url,
+                cgi.escape(self.displayname, quote=True),
+                displayname)
+
+    @property
+    def icons_and_name(self):
+        """Icon list and name, linked to changes file if appropriate."""
+        iconlist_id = "queue%d-iconlist" % self.id
+        icons = self.composeIconList()
+        link = self.composeNameAndChangesLink()
+        return """
+            <div id="%s">
+              %s
+              %s
+              (%s)
+            </div>
+            """ % (iconlist_id, '\n'.join(icons), link, self.displayarchs)

=== modified file 'lib/lp/soyuz/browser/tests/test_queue.py'
--- lib/lp/soyuz/browser/tests/test_queue.py	2011-06-16 13:21:14 +0000
+++ lib/lp/soyuz/browser/tests/test_queue.py	2011-06-21 05:48:36 +0000
@@ -5,6 +5,8 @@
 
 __metaclass__ = type
 
+import cgi
+from lxml import html
 import transaction
 from zope.component import (
     getUtility,
@@ -12,8 +14,12 @@
     )
 
 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
-from canonical.testing.layers import LaunchpadFunctionalLayer
+from canonical.testing.layers import (
+    LaunchpadFunctionalLayer,
+    LaunchpadZopelessLayer,
+    )
 from lp.archiveuploader.tests import datadir
+from lp.soyuz.browser.queue import CompletePackageUpload
 from lp.soyuz.enums import PackageUploadStatus
 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
 from lp.soyuz.interfaces.queue import IPackageUploadSet
@@ -208,8 +214,8 @@
             upload.distroseries.main_archive)
         with person_logged_in(queue_admin):
             view = self.makeView(upload.distroseries, queue_admin)
-            html = view()
-        self.assertIn(upload.package_name, html)
+            html_text = view()
+        self.assertIn(upload.package_name, html_text)
 
     def test_view_renders_build_upload(self):
         login(ADMIN_EMAIL)
@@ -218,8 +224,8 @@
             upload.distroseries.main_archive)
         with person_logged_in(queue_admin):
             view = self.makeView(upload.distroseries, queue_admin)
-            html = view()
-        self.assertIn(upload.package_name, html)
+            html_text = view()
+        self.assertIn(upload.package_name, html_text)
 
     def test_view_renders_copy_upload(self):
         login(ADMIN_EMAIL)
@@ -228,5 +234,166 @@
             upload.distroseries.main_archive)
         with person_logged_in(queue_admin):
             view = self.makeView(upload.distroseries, queue_admin)
-            html = view()
-        self.assertIn(upload.package_name, html)
+            html_text = view()
+        self.assertIn(upload.package_name, html_text)
+
+
+class TestCompletePackageUpload(TestCaseWithFactory):
+
+    layer = LaunchpadZopelessLayer
+
+    def makeCompletePackageUpload(self, upload=None, build_upload_files=None,
+                                  source_upload_files=None,
+                                  package_sets=None):
+        if upload is None:
+            upload = self.factory.makeSourcePackageUpload()
+        if build_upload_files is None:
+            build_upload_files = {}
+        if source_upload_files is None:
+            source_upload_files = {}
+        if package_sets is None:
+            package_sets = {}
+        return CompletePackageUpload(
+            upload, build_upload_files, source_upload_files, package_sets)
+
+    def mapPackageSets(self, upload, package_sets=None):
+        if package_sets is None:
+            package_sets = [self.factory.makePackageset(
+                distroseries=upload.distroseries)]
+        spn = upload.sourcepackagerelease.sourcepackagename
+        return {spn.id: package_sets}
+
+    def test_display_package_sets_returns_source_upload_packagesets(self):
+        upload = self.factory.makeSourcePackageUpload()
+        package_sets = self.mapPackageSets(upload)
+        complete_upload = self.makeCompletePackageUpload(
+            upload, package_sets=package_sets)
+        self.assertEqual(
+            package_sets.values()[0][0].name,
+            complete_upload.display_package_sets)
+
+    def test_display_package_sets_returns_empty_for_non_source_upload(self):
+        upload = self.factory.makeBuildPackageUpload()
+        complete_upload = self.makeCompletePackageUpload(
+            upload, package_sets=self.mapPackageSets(upload))
+        self.assertEqual("", complete_upload.display_package_sets)
+
+    def test_display_package_sets_sorts_by_name(self):
+        complete_upload = self.makeCompletePackageUpload()
+        distroseries = complete_upload.distroseries
+        complete_upload.package_sets = [
+            self.factory.makePackageset(distroseries=distroseries, name=name)
+            for name in [u'ccc', u'aaa', u'bbb']]
+        self.assertEqual("aaa bbb ccc", complete_upload.display_package_sets)
+
+    def test_display_component_returns_source_upload_component_name(self):
+        complete_upload = self.makeCompletePackageUpload()
+        self.assertEqual(
+            complete_upload.sourcepackagerelease.component.name.lower(),
+            complete_upload.display_component)
+
+    def test_display_component_returns_empty_for_non_source_upload(self):
+        complete_upload = self.makeCompletePackageUpload(
+            self.factory.makeBuildPackageUpload())
+        self.assertEqual('', complete_upload.display_component)
+
+    def test_display_section_returns_source_upload_section_name(self):
+        complete_upload = self.makeCompletePackageUpload()
+        self.assertEqual(
+            complete_upload.sourcepackagerelease.section.name.lower(),
+            complete_upload.display_section)
+
+    def test_display_section_returns_empty_for_non_source_upload(self):
+        complete_upload = self.makeCompletePackageUpload(
+            self.factory.makeBuildPackageUpload())
+        self.assertEqual('', complete_upload.display_section)
+
+    def test_display_priority_returns_source_upload_priority(self):
+        complete_upload = self.makeCompletePackageUpload()
+        self.assertEqual(
+            complete_upload.sourcepackagerelease.urgency.name.lower(),
+            complete_upload.display_priority)
+
+    def test_display_priority_returns_empty_for_non_source_upload(self):
+        complete_upload = self.makeCompletePackageUpload(
+            self.factory.makeBuildPackageUpload())
+        self.assertEqual('', complete_upload.display_priority)
+
+    def test_composeIcon_produces_image_tag(self):
+        alt = self.factory.getUniqueString()
+        icon = self.factory.getUniqueString() + ".png"
+        title = self.factory.getUniqueString()
+        html_text = self.makeCompletePackageUpload().composeIcon(
+            alt, icon, title)
+        img = html.fromstring(html_text)
+        self.assertEqual("img", img.tag)
+        self.assertEqual("[%s]" % alt, img.get("alt"))
+        self.assertEqual("/@@/" + icon, img.get("src"))
+        self.assertEqual(title, img.get("title"))
+
+    def test_composeIcon_title_defaults_to_alt_text(self):
+        alt = self.factory.getUniqueString()
+        icon = self.factory.getUniqueString() + ".png"
+        html_text = self.makeCompletePackageUpload().composeIcon(alt, icon)
+        img = html.fromstring(html_text)
+        self.assertEqual(alt, img.get("title"))
+
+    def test_composeIcon_escapes_alt_and_title(self):
+        alt = 'alt"&'
+        icon = self.factory.getUniqueString() + ".png"
+        title = 'title"&'
+        html_text = self.makeCompletePackageUpload().composeIcon(
+            alt, icon, title)
+        img = html.fromstring(html_text)
+        self.assertEqual("[%s]" % alt, img.get("alt"))
+        self.assertEqual(title, img.get("title"))
+
+    def test_composeIconList_produces_icons(self):
+        icons = self.makeCompletePackageUpload().composeIconList()
+        self.assertNotEqual([], icons)
+        self.assertEqual('img', html.fromstring(icons[0]).tag)
+
+    def test_composeIconList_produces_icons_conditionally(self):
+        complete_upload = self.makeCompletePackageUpload()
+        base_count = len(complete_upload.composeIconList())
+        complete_upload.contains_build = True
+        new_count = len(complete_upload.composeIconList())
+        self.assertEqual(base_count + 1, new_count)
+
+    def test_composeNameAndChangesLink_does_not_link_if_no_changes_file(self):
+        upload = self.factory.makeCopyJobPackageUpload()
+        complete_upload = self.makeCompletePackageUpload(upload)
+        self.assertEqual(
+            complete_upload.displayname,
+            complete_upload.composeNameAndChangesLink())
+
+    def test_composeNameAndChangesLink_links_to_changes_file(self):
+        complete_upload = self.makeCompletePackageUpload()
+        link = html.fromstring(complete_upload.composeNameAndChangesLink())
+        self.assertEqual(
+            complete_upload.changesfile.http_url, link.get("href"))
+
+    def test_composeNameAndChangesLink_escapes_nonlinked_display_name(self):
+        filename = 'name"&name'
+        upload = self.factory.makeCustomPackageUpload(filename=filename)
+        # Stop nameAndChangesLink from producing a link.
+        upload.changesfile = None
+        complete_upload = self.makeCompletePackageUpload(upload)
+        self.assertIn(
+            cgi.escape(filename), complete_upload.composeNameAndChangesLink())
+
+    def test_composeNameAndChangesLink_escapes_name_in_link(self):
+        filename = 'name"&name'
+        upload = self.factory.makeCustomPackageUpload(filename=filename)
+        complete_upload = self.makeCompletePackageUpload(upload)
+        link = html.fromstring(complete_upload.composeNameAndChangesLink())
+        self.assertIn(filename, link.get("title"))
+        self.assertEqual(filename, link.text)
+
+    def test_icons_and_name_composes_icons_and_link_and_archs(self):
+        complete_upload = self.makeCompletePackageUpload()
+        icons_and_name = html.fromstring(complete_upload.icons_and_name)
+        self.assertNotEqual(None, icons_and_name.find("img"))
+        self.assertNotEqual(None, icons_and_name.find("a"))
+        self.assertIn(
+            complete_upload.displayarchs, ' '.join(icons_and_name.itertext()))

=== modified file 'lib/lp/soyuz/interfaces/packageset.py'
--- lib/lp/soyuz/interfaces/packageset.py	2011-06-16 18:51:45 +0000
+++ lib/lp/soyuz/interfaces/packageset.py	2011-06-21 05:48:36 +0000
@@ -445,6 +445,17 @@
         :return: An iterable collection of `IPackageset` instances.
         """
 
+    def getForPackages(distroseries, sourcepackagename_ids):
+        """Get `Packagesets` that directly contain the given packages.
+
+        :param distroseries: `DistroSeries` to look in.  Only packagesets for
+            this series will be returned.
+        :param sourcepackagename_ids: A sequence of `SourcePackageName` ids.
+            Only packagesets for these package names will be returned.
+        :return: A dict mapping `SourcePackageName` ids to lists of their
+            respective packagesets, in no particular order.
+        """
+
     @operation_parameters(
         sourcepackagename=TextLine(
             title=_('Source package name'), required=True),

=== modified file 'lib/lp/soyuz/model/packageset.py'
--- lib/lp/soyuz/model/packageset.py	2011-03-22 11:30:13 +0000
+++ lib/lp/soyuz/model/packageset.py	2011-06-21 05:48:36 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -36,6 +36,7 @@
     NoSuchPackageSet,
     )
 from lp.soyuz.model.packagesetgroup import PackagesetGroup
+from lp.soyuz.model.packagesetsources import PackagesetSources
 
 
 def _order_result_set(result_set):
@@ -427,23 +428,36 @@
             source_name = getUtility(ISourcePackageNameSet)[source_name]
         return source_name
 
+    def getForPackages(self, distroseries, sourcepackagename_ids):
+        """See `IPackagesetSet`."""
+        tuples = IStore(Packageset).find(
+            (PackagesetSources.sourcepackagename_id, Packageset),
+            Packageset.id == PackagesetSources.packageset_id,
+            Packageset.distroseries == distroseries,
+            PackagesetSources.sourcepackagename_id.is_in(
+                sourcepackagename_ids))
+        packagesets_by_package = {}
+        for package, packageset in tuples:
+            packagesets_by_package.setdefault(package, []).append(packageset)
+        return packagesets_by_package
+
     def setsIncludingSource(self, sourcepackagename, distroseries=None,
                             direct_inclusion=False):
         """See `IPackagesetSet`."""
         sourcepackagename = self._nameToSourcePackageName(sourcepackagename)
 
-        if direct_inclusion == False:
+        if direct_inclusion:
+            query = '''
+                SELECT pss.packageset FROM packagesetsources pss
+                WHERE pss.sourcepackagename = ?
+            '''
+        else:
             query = '''
                 SELECT fpsi.parent
                 FROM packagesetsources pss, flatpackagesetinclusion fpsi
                 WHERE pss.sourcepackagename = ?
                 AND pss.packageset = fpsi.child
             '''
-        else:
-            query = '''
-                SELECT pss.packageset FROM packagesetsources pss
-                WHERE pss.sourcepackagename = ?
-            '''
         store = IStore(Packageset)
         psets = SQL(query, (sourcepackagename.id,))
         clauses = [Packageset.id.is_in(psets)]

=== added file 'lib/lp/soyuz/model/packagesetsources.py'
--- lib/lp/soyuz/model/packagesetsources.py	1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/model/packagesetsources.py	2011-06-21 05:48:36 +0000
@@ -0,0 +1,40 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""The `PackagesetSources` linking table.
+
+This table associates `Packageset`s with `SourcePackageName`s.
+"""
+
+__metaclass__ = type
+__all__ = [
+    'PackagesetSources',
+    ]
+
+from storm.locals import (
+    Int,
+    Reference,
+    Storm,
+    )
+
+
+class PackagesetSources(Storm):
+    """Linking table: which packages are in a package set?"""
+    # This table is largely managed from Packageset, but also directly
+    # accessed from other places.
+
+    __storm_table__ = 'PackagesetSources'
+
+    # There's a vestigial id as well, a holdover from the SQLObject
+    # days.  Nobody seems to use it.  The only key that matters is
+    # (packageset, sourcepackagename).
+    __storm_primary__ = (
+        'packageset_id',
+        'sourcepackagename_id',
+        )
+
+    packageset_id = Int(name='packageset')
+    packageset = Reference(packageset_id, 'Packageset.id')
+    sourcepackagename_id = Int(name='sourcepackagename')
+    sourcepackagename = Reference(
+        sourcepackagename_id, 'SourcePackageName.id')

=== modified file 'lib/lp/soyuz/templates/distroseries-queue.pt'
--- lib/lp/soyuz/templates/distroseries-queue.pt	2011-06-16 13:21:14 +0000
+++ lib/lp/soyuz/templates/distroseries-queue.pt	2011-06-21 05:48:36 +0000
@@ -89,33 +89,30 @@
                      <input type="checkbox" name="QUEUE_ID"
                             tal:attributes="value packageupload/id"/>
                 </td>
-                <td style="padding-top: 5px">
-                  <metal:iconlist use-macro="template/macros/package-iconlist"/>
+                <td
+                  style="padding-top: 5px"
+                  tal:content="structure packageupload/icons_and_name">
                 </td>
 
                 <td style="padding-top: 5px"
                     tal:content="packageupload/displayversion">2.0.17-6
                 </td>
                 <tal:is_source define="is_source packageupload/contains_source">
-                  <td style="padding-top: 5px">
-                    <tal:component condition="is_source"
-                         content="packageupload/sourcepackagerelease/component/name/lower">
-                    </tal:component>
-                  </td>
-                  <td style="padding-top: 5px">
-                    <tal:section condition="is_source"
-                         content="packageupload/sourcepackagerelease/section/name/lower">
-                    </tal:section>
-                  </td>
-                  <td style="padding-top: 5px">
-                    <tal:priority condition="is_source"
-                         content="packageupload/sourcepackagerelease/urgency/name/lower">
-                    </tal:priority>
-                  </td>
-                  <td style="padding-top: 5px">
-                    <tal:packagesets condition="is_source"
-                         content="packageupload/package_sets">
-                    </tal:packagesets>
+                  <td
+                    style="padding-top: 5px"
+                    tal:content="packageupload/display_component">
+                  </td>
+                  <td
+                    style="padding-top: 5px"
+                    tal:content="packageupload/display_section">
+                  </td>
+                  <td
+                    style="padding-top: 5px"
+                    tal:content="packageupload/display_priority">
+                  </td>
+                  <td
+                    style="padding-top: 5px"
+                    tal:content="packageupload/display_package_sets">
                   </td>
                 </tal:is_source>
                 <td style="padding-top: 5px"
@@ -232,7 +229,9 @@
           style="display:none">
         <td/>
         <td tal:condition="view/availableActions"/>
-        <td tal:define="libraryfilealias file/libraryfile">
+        <td
+          tal:define="libraryfilealias file/libraryfile"
+          tal:condition="libraryfilealias">
           <metal:file use-macro="template/macros/package-file"/>
         </td>
         <td colspan="6"/>
@@ -298,59 +297,15 @@
     :libraryfilealias: A LibraryFileAlias to link to. If it is expired,
       no link will be created.
   </tal:comment>
-  <tal:not-none condition="libraryfilealias">
-    <tal:unexpired tal:condition="libraryfilealias/content">
-      <a tal:attributes="href libraryfilealias/http_url">
-        <tal:filename replace="libraryfilealias/filename"/>
-      </a>
-      (<span tal:replace="libraryfilealias/content/filesize/fmt:bytes" />)
-    </tal:unexpired>
-    <tal:expired tal:condition="not:libraryfilealias/content">
-      <span tal:content="libraryfilealias/filename"/>
-    </tal:expired>
-  </tal:not-none>
-</metal:macro>
-
-<metal:macro define-macro="package-iconlist">
-  <tal:comment replace="nothing">
-    This macro expects the following variables defined:
-    :packageupload: A PackageUpload record for which we display icons.
-  </tal:comment>
-
-  <div tal:attributes="id string:queue${packageupload/id}-iconlist">
-    <img tal:condition="packageupload/contains_source"
-         alt="[Source]" src="/@@/package-source" title="Source"/>
-    <img tal:condition="packageupload/contains_build"
-         alt="[Build]" src="/@@/package-binary" title="Binary"/>
-    <img tal:condition="packageupload/contains_translation"
-         alt="[Translation]" src="/@@/translation-file"
-         title="Translation"/>
-    <img tal:condition="packageupload/contains_installer"
-         alt="[Installer]" src="/@@/ubuntu-icon"
-         title="Installer"/>
-    <img tal:condition="packageupload/contains_upgrader"
-         alt="[Upgrader]" src="/@@/ubuntu-icon"
-         title="Upgrader"/>
-    <img tal:condition="packageupload/contains_ddtp"
-         alt="[Debian Description Translation Project Indexes]"
-         src="/@@/ubuntu-icon"
-         title="Debian Description Translation Project Indexes"/>
-    <tal:not-delayed condition="not: packageupload/pending_delayed_copy">
-      <a tal:condition="packageupload/changesfile"
-         tal:content="packageupload/displayname"
-         tal:attributes="
-           href packageupload/changesfile/http_url;
-           title string:Changes file for ${packageupload/displayname};">
-      </a>
-      <tal:no-changes-file
-        condition="not: packageupload/changesfile"
-        replace="packageupload/displayname"/>
-    </tal:not-delayed>
-    <tal:pending_delayed_copy_title
-      condition="packageupload/pending_delayed_copy"
-      replace="packageupload/displayname" />
-    <tal:arches replace="string: (${packageupload/displayarchs})"/>
-  </div>
+  <tal:unexpired tal:condition="libraryfilealias/content">
+    <a tal:attributes="href libraryfilealias/http_url">
+      <tal:filename replace="libraryfilealias/filename"/>
+    </a>
+    (<span tal:replace="libraryfilealias/content/filesize/fmt:bytes" />)
+  </tal:unexpired>
+  <tal:expired tal:condition="not:libraryfilealias/content">
+    <span tal:content="libraryfilealias/filename"/>
+  </tal:expired>
 </metal:macro>
 
 </metal:macros>

=== modified file 'lib/lp/soyuz/tests/test_packageset.py'
--- lib/lp/soyuz/tests/test_packageset.py	2011-03-22 11:30:13 +0000
+++ lib/lp/soyuz/tests/test_packageset.py	2011-06-21 05:48:36 +0000
@@ -1,11 +1,11 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test Packageset features."""
 
 from zope.component import getUtility
 
-from canonical.testing.layers import LaunchpadZopelessLayer
+from canonical.testing.layers import ZopelessDatabaseLayer
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.series import SeriesStatus
 from lp.soyuz.interfaces.packageset import (
@@ -17,128 +17,154 @@
 
 class TestPackagesetSet(TestCaseWithFactory):
 
-    layer = LaunchpadZopelessLayer
-
-    def setUp(self):
-        """Setup a distribution with multiple distroseries."""
-        super(TestPackagesetSet, self).setUp()
-        self.distribution = getUtility(IDistributionSet).getByName(
-            'ubuntu')
-        self.distroseries_current = self.distribution.currentseries
-        self.distroseries_experimental = self.factory.makeDistroRelease(
-            distribution = self.distribution, name="experimental",
+    layer = ZopelessDatabaseLayer
+
+    def getUbuntu(self):
+        """Get the Ubuntu `Distribution`."""
+        return getUtility(IDistributionSet).getByName('ubuntu')
+
+    def makeExperimentalSeries(self):
+        """Create an experimental Ubuntu `DistroSeries`."""
+        return self.factory.makeDistroSeries(
+            distribution=self.getUbuntu(), name="experimental",
             status=SeriesStatus.EXPERIMENTAL)
 
-        self.person1 = self.factory.makePerson(
-            name='hacker', displayname=u'Happy Hacker')
-
-        self.packageset_set = getUtility(IPackagesetSet)
-
     def test_new_defaults_to_current_distroseries(self):
         # If the distroseries is not provided, the current development
         # distroseries will be assumed.
-        packageset = self.packageset_set.new(
-            u'kernel', u'Contains all OS kernel packages', self.person1)
-
+        packageset = getUtility(IPackagesetSet).new(
+            self.factory.getUniqueUnicode(), self.factory.getUniqueUnicode(),
+            self.factory.makePerson())
         self.failUnlessEqual(
-            self.distroseries_current, packageset.distroseries)
+            self.getUbuntu().currentseries, packageset.distroseries)
 
     def test_new_with_specified_distroseries(self):
         # A distroseries can be provided when creating a package set.
-        packageset = self.packageset_set.new(
-            u'kernel', u'Contains all OS kernel packages', self.person1,
-            distroseries=self.distroseries_experimental)
-
-        self.failUnlessEqual(
-            self.distroseries_experimental, packageset.distroseries)
+        experimental_series = self.makeExperimentalSeries()
+        packageset = getUtility(IPackagesetSet).new(
+            self.factory.getUniqueUnicode(), self.factory.getUniqueUnicode(),
+            self.factory.makePerson(), distroseries=experimental_series)
+        self.failUnlessEqual(experimental_series, packageset.distroseries)
 
     def test_new_creates_new_packageset_group(self):
         # Creating a new packageset should also create a new packageset
         # group with the same owner.
-        packageset = self.packageset_set.new(
-            u'kernel', u'Contains all OS kernel packages', self.person1,
-            distroseries=self.distroseries_experimental)
-
-        self.failUnlessEqual(
-            self.person1, packageset.packagesetgroup.owner)
+        owner = self.factory.makePerson()
+        experimental_series = self.makeExperimentalSeries()
+        packageset = getUtility(IPackagesetSet).new(
+            self.factory.getUniqueUnicode(), self.factory.getUniqueUnicode(),
+            owner, distroseries=experimental_series)
+        self.failUnlessEqual(owner, packageset.packagesetgroup.owner)
 
     def test_new_duplicate_name_for_same_distroseries(self):
         # Creating a packageset with a duplicate name for the
         # given distroseries will fail.
-        packageset = self.packageset_set.new(
-            u'kernel', u'Contains all OS kernel packages', self.person1,
-            distroseries=self.distroseries_experimental)
-
-        self.failUnlessRaises(
-            DuplicatePackagesetName, self.packageset_set.new,
-            u'kernel', u'A packageset with a duplicate name', self.person1,
-            distroseries=self.distroseries_experimental)
+        distroseries = self.factory.makeDistroSeries()
+        name = self.factory.getUniqueUnicode()
+        self.factory.makePackageset(name, distroseries=distroseries)
+        self.assertRaises(
+            DuplicatePackagesetName, getUtility(IPackagesetSet).new,
+            name, self.factory.getUniqueUnicode(), self.factory.makePerson(),
+            distroseries=distroseries)
 
     def test_new_duplicate_name_for_different_distroseries(self):
         # Creating a packageset with a duplicate name but for a different
         # series is no problem.
-        packageset = self.packageset_set.new(
-            u'kernel', u'Contains all OS kernel packages', self.person1)
-
-        packageset2 = self.packageset_set.new(
-            u'kernel', u'A packageset with a duplicate name', self.person1,
-            distroseries=self.distroseries_experimental)
-        self.assertEqual(packageset.name, packageset2.name)
+        name = self.factory.getUniqueUnicode()
+        packageset1 = self.factory.makePackageset(name)
+        packageset2 = getUtility(IPackagesetSet).new(
+            name, self.factory.getUniqueUnicode(), self.factory.makePerson(),
+            distroseries=self.factory.makeDistroSeries())
+        self.assertEqual(packageset1.name, packageset2.name)
 
     def test_new_related_packageset(self):
         # Creating a new package set while specifying a `related_set` should
         # have the effect that the former ends up in the same group as the
         # latter.
-        pset1 = self.packageset_set.new(
-            u'kernel', u'Contains all OS kernel packages', self.person1)
-
-        pset2 = self.packageset_set.new(
-            u'kernel', u'A related package set.', self.person1,
-            distroseries=self.distroseries_experimental, related_set=pset1)
+        name = self.factory.getUniqueUnicode()
+        pset1 = self.factory.makePackageset(name)
+        pset2 = self.factory.makePackageset(
+            name, distroseries=self.makeExperimentalSeries(),
+            related_set=pset1)
         self.assertEqual(pset1.packagesetgroup, pset2.packagesetgroup)
 
     def test_get_by_name_in_current_distroseries(self):
         # IPackagesetSet.getByName() will return the package set in the
         # current distroseries if the optional `distroseries` parameter is
         # omitted.
-        pset1 = self.packageset_set.new(
-            u'kernel', u'Contains all OS kernel packages', self.person1)
-        pset2 = self.packageset_set.new(
-            u'kernel', u'A related package set.', self.person1,
-            distroseries=self.distroseries_experimental, related_set=pset1)
-        pset_found = getUtility(IPackagesetSet).getByName('kernel')
-        self.assertEqual(pset1, pset_found)
+        name = self.factory.getUniqueUnicode()
+        pset1 = self.factory.makePackageset(name)
+        self.factory.makePackageset(
+            name, distroseries=self.makeExperimentalSeries(),
+            related_set=pset1)
+        self.assertEqual(pset1, getUtility(IPackagesetSet).getByName(name))
 
     def test_get_by_name_in_specified_distroseries(self):
         # IPackagesetSet.getByName() will return the package set in the
         # specified distroseries.
-        pset1 = self.packageset_set.new(
-            u'kernel', u'Contains all OS kernel packages', self.person1)
-        pset2 = self.packageset_set.new(
-            u'kernel', u'A related package set.', self.person1,
-            distroseries=self.distroseries_experimental, related_set=pset1)
+        name = self.factory.getUniqueUnicode()
+        experimental_series = self.makeExperimentalSeries()
+        pset1 = self.factory.makePackageset(name)
+        pset2 = self.factory.makePackageset(
+            name, distroseries=experimental_series, related_set=pset1)
         pset_found = getUtility(IPackagesetSet).getByName(
-            'kernel', distroseries=self.distroseries_experimental)
+            name, distroseries=experimental_series)
         self.assertEqual(pset2, pset_found)
 
     def test_get_by_distroseries(self):
         # IPackagesetSet.getBySeries() will return those package sets
         # associated with the given distroseries.
-        pset1 = self.packageset_set.new(
-            u'timmy', u'Timmy Mallett', self.person1)
-        pset2 = self.packageset_set.new(
-            u'savile', u'Jimmy Savile', self.person1)
-        self.packageset_set.new(
-            u'hoskins', u'Bob Hoskins', self.person1,
-            distroseries=self.distroseries_experimental)
+        package_sets_for_current_ubuntu = [
+            self.factory.makePackageset() for counter in xrange(2)]
+        self.factory.makePackageset(
+            distroseries=self.makeExperimentalSeries())
         self.assertContentEqual(
-            [pset1, pset2],
-            self.packageset_set.getBySeries(self.distroseries_current))
+            package_sets_for_current_ubuntu,
+            getUtility(IPackagesetSet).getBySeries(
+                self.getUbuntu().currentseries))
+
+    def test_getForPackages_returns_packagesets(self):
+        # getForPackages finds package sets for given source package
+        # names in a distroseries, and maps them by
+        # SourcePackageName.id.
+        series = self.factory.makeDistroSeries()
+        packageset = self.factory.makePackageset(distroseries=series)
+        package = self.factory.makeSourcePackageName()
+        packageset.addSources([package.name])
+        self.assertEqual(
+            {package.id: [packageset]},
+            getUtility(IPackagesetSet).getForPackages(series, [package.id]))
+
+    def test_getForPackages_filters_by_distroseries(self):
+        # getForPackages does not return packagesets for different
+        # distroseries.
+        series = self.factory.makeDistroSeries()
+        other_series = self.factory.makeDistroSeries()
+        packageset = self.factory.makePackageset(distroseries=series)
+        package = self.factory.makeSourcePackageName()
+        packageset.addSources([package.name])
+        self.assertEqual(
+            {},
+            getUtility(IPackagesetSet).getForPackages(
+                other_series, [package.id]))
+
+    def test_getForPackages_filters_by_sourcepackagename(self):
+        # getForPackages does not return packagesets for different
+        # source package names.
+        series = self.factory.makeDistroSeries()
+        packageset = self.factory.makePackageset(distroseries=series)
+        package = self.factory.makeSourcePackageName()
+        other_package = self.factory.makeSourcePackageName()
+        packageset.addSources([package.name])
+        self.assertEqual(
+            {},
+            getUtility(IPackagesetSet).getForPackages(
+                series, [other_package.id]))
 
 
 class TestPackageset(TestCaseWithFactory):
 
-    layer = LaunchpadZopelessLayer
+    layer = ZopelessDatabaseLayer
 
     def setUp(self):
         """Setup a distribution with multiple distroseries."""
@@ -147,10 +173,10 @@
             'ubuntu')
         self.distroseries_current = self.distribution.currentseries
         self.distroseries_experimental = self.factory.makeDistroRelease(
-            distribution = self.distribution, name="experimental",
+            distribution=self.distribution, name="experimental",
             status=SeriesStatus.EXPERIMENTAL)
         self.distroseries_experimental2 = self.factory.makeDistroRelease(
-            distribution = self.distribution, name="experimental2",
+            distribution=self.distribution, name="experimental2",
             status=SeriesStatus.EXPERIMENTAL)
 
         self.person1 = self.factory.makePerson(


Follow ups