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

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 13280
Proposed branch: lp:~jtv/launchpad/bug-798521
Merge into: lp:launchpad
Diff against target: 1043 lines (+519/-190)
9 files modified
lib/lp/registry/model/distroseriesdifference.py (+5/-6)
lib/lp/soyuz/browser/queue.py (+121/-12)
lib/lp/soyuz/browser/tests/test_queue.py (+174/-7)
lib/lp/soyuz/interfaces/files.py (+11/-4)
lib/lp/soyuz/interfaces/packageset.py (+11/-0)
lib/lp/soyuz/model/packageset.py (+21/-7)
lib/lp/soyuz/model/packagesetsources.py (+41/-0)
lib/lp/soyuz/templates/distroseries-queue.pt (+30/-75)
lib/lp/soyuz/tests/test_packageset.py (+105/-79)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-798521
Reviewer Review Type Date Requested Status
Данило Шеган (community) Approve
Review via email: mp+65297@code.launchpad.net

Commit message

[r=danilo][bug=798521] Clean up +queue page generation.

Description of the change

= 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 Packagesets associated with the SourcePackages related to SourcePackageReleases for source uploads.

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

To post a comment you must log in.
Revision history for this message
Данило Шеган (danilo) wrote :
Download full text (8.1 KiB)

Hi Jeroen,

Thanks for doing this. As with any refactoring and cleanup, it always
invites more of the same. Please ignore any of such requests below if
you don't feel like doing it. The only thing I care about is XSS attack
vectors, so please ensure that no trouble can come out of it. As long
as that's fine, feel free to land.

  review approve

> === modified file 'lib/lp/soyuz/browser/queue.py'
> @@ -176,6 +182,15 @@
> # Listify to avoid repeated queries.
> return list(old_binary_packages)
>
> + def getPackagesetsFor(self, source_package_releases):
> + """Find associated `Packagesets`.
> +
> + :param source_package_releases: A sequence of
`SourcePackageRelease`s.
> + """
> + sprs = [spr for spr in source_package_releases if spr is not
None]
> + return getUtility(IPackagesetSet).getForPackages(
> + self.context, set(spr.sourcepackagenameID for spr in
sprs))

Any reason you are not constructing a set directly in the line above?

> + packageuploadsources = load_referencing(
> + source_sprs = load_related(

Didn't know about these, cool stuff :)

> @@ -217,9 +241,11 @@
> self.old_binary_packages = self.calculateOldBinaries(
> binary_package_names)
>
> + package_sets = self.getPackagesetsFor(source_sprs)

I tried to restrain myself, but I really hate "Packageset" spelling. I
am assuming this was a decision made some time ago, but I always expect
it to be PackageSet, even though they might imply some other things in
Launchpad.

(The fact that you are spelling it as package_sets when lowercase seems
to imply that you agree)

> @@ -415,8 +441,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.

What I don't understand is why is this not a view in the first place?

> @@ -459,6 +486,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 +511,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)))

Is self.contains_source equival...

Read more...

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (7.6 KiB)

Thanks for the review!

> > === modified file 'lib/lp/soyuz/browser/queue.py'
> > @@ -176,6 +182,15 @@
> > # Listify to avoid repeated queries.
> > return list(old_binary_packages)
> >
> > + def getPackagesetsFor(self, source_package_releases):
> > + """Find associated `Packagesets`.
> > +
> > + :param source_package_releases: A sequence of
> `SourcePackageRelease`s.
> > + """
> > + sprs = [spr for spr in source_package_releases if spr is not
> None]
> > + return getUtility(IPackagesetSet).getForPackages(
> > + self.context, set(spr.sourcepackagenameID for spr in
> sprs))
>
> Any reason you are not constructing a set directly in the line above?

I spelled this in various ways over time, but found that breaking it up in this way keeps both individual lines nicely legible.

> > @@ -217,9 +241,11 @@
> > self.old_binary_packages = self.calculateOldBinaries(
> > binary_package_names)
> >
> > + package_sets = self.getPackagesetsFor(source_sprs)
>
> I tried to restrain myself, but I really hate "Packageset" spelling. I
> am assuming this was a decision made some time ago, but I always expect
> it to be PackageSet, even though they might imply some other things in
> Launchpad.
>
> (The fact that you are spelling it as package_sets when lowercase seems
> to imply that you agree)

I do. I got it wrong many times while working on this branch.

> > @@ -415,8 +441,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.
>
> What I don't understand is why is this not a view in the first place?

With all the logic in the TAL, this class started out purely as a decorator for bulk-fetching some attributes on PackageUpload.

> > @@ -478,11 +511,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)))
>
> Is self.contains_source equivalent to self.sourcepackagerelease? Any
> reason you are dropping the assertion and replacing it with empty
> package_sets?

It's not equivalent, no. The reason I removed the assertion is that what it says is no longer true. We wish to display this information for sync uploads as well, though there it will need to be retrieved in different ways.

> > + def composeIcon(self, alt, icon, title=None):
> > + """Compose an icon for the package's icon lis...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
2--- lib/lp/registry/model/distroseriesdifference.py 2011-06-14 13:47:51 +0000
3+++ lib/lp/registry/model/distroseriesdifference.py 2011-06-22 13:09:13 +0000
4@@ -97,6 +97,7 @@
5 DistroSeriesSourcePackageRelease,
6 )
7 from lp.soyuz.model.packageset import Packageset
8+from lp.soyuz.model.packagesetsources import PackagesetSources
9 from lp.soyuz.model.publishing import SourcePackagePublishingHistory
10 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
11
12@@ -205,7 +206,6 @@
13 if len(dsds) == 0:
14 return {}
15
16- PackagesetSources = Table("PackageSetSources")
17 FlatPackagesetInclusion = Table("FlatPackagesetInclusion")
18
19 tables = IStore(Packageset).using(
20@@ -213,18 +213,17 @@
21 PackagesetSources, FlatPackagesetInclusion)
22 results = tables.find(
23 (DistroSeriesDifference.id, Packageset),
24- Column("packageset", PackagesetSources) == (
25- Column("child", FlatPackagesetInclusion)),
26+ PackagesetSources.packageset_id == Column(
27+ "child", FlatPackagesetInclusion),
28 Packageset.distroseries_id == (
29 DistroSeriesDifference.parent_series_id if in_parent else
30 DistroSeriesDifference.derived_series_id),
31 Column("parent", FlatPackagesetInclusion) == Packageset.id,
32- Column("sourcepackagename", PackagesetSources) == (
33+ PackagesetSources.sourcepackagename_id == (
34 DistroSeriesDifference.source_package_name_id),
35 DistroSeriesDifference.id.is_in(dsd.id for dsd in dsds))
36 results = results.order_by(
37- Column("sourcepackagename", PackagesetSources),
38- Packageset.name)
39+ PackagesetSources.sourcepackagename_id, Packageset.name)
40
41 grouped = defaultdict(list)
42 for dsd_id, packageset in results:
43
44=== modified file 'lib/lp/soyuz/browser/queue.py'
45--- lib/lp/soyuz/browser/queue.py 2011-06-16 10:44:00 +0000
46+++ lib/lp/soyuz/browser/queue.py 2011-06-22 13:09:13 +0000
47@@ -9,6 +9,7 @@
48 'QueueItemsView',
49 ]
50
51+import cgi
52 import operator
53
54 from lazr.delegates import delegates
55@@ -22,6 +23,10 @@
56 NotFoundError,
57 UnexpectedFormData,
58 )
59+from lp.services.database.bulk import (
60+ load_referencing,
61+ load_related,
62+ )
63 from lp.soyuz.enums import (
64 PackagePublishingPriority,
65 PackageUploadStatus,
66@@ -41,6 +46,7 @@
67 QueueInconsistentStateError,
68 )
69 from lp.soyuz.interfaces.section import ISectionSet
70+from lp.soyuz.model.queue import PackageUploadSource
71
72
73 QUEUE_SIZE = 30
74@@ -176,6 +182,15 @@
75 # Listify to avoid repeated queries.
76 return list(old_binary_packages)
77
78+ def getPackagesetsFor(self, source_package_releases):
79+ """Find associated `Packagesets`.
80+
81+ :param source_package_releases: A sequence of `SourcePackageRelease`s.
82+ """
83+ sprs = [spr for spr in source_package_releases if spr is not None]
84+ return getUtility(IPackagesetSet).getForPackages(
85+ self.context, set(spr.sourcepackagenameID for spr in sprs))
86+
87 def decoratedQueueBatch(self):
88 """Return the current batch, converted to decorated objects.
89
90@@ -183,6 +198,9 @@
91 CompletePackageUpload. This avoids many additional SQL queries
92 in the +queue template.
93 """
94+ # Avoid circular imports.
95+ from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
96+
97 uploads = list(self.batchnav.currentBatch())
98
99 if len(uploads) == 0:
100@@ -196,9 +214,15 @@
101 upload.status != PackageUploadStatus.DONE)]
102 binary_file_set = getUtility(IBinaryPackageFileSet)
103 binary_files = binary_file_set.getByPackageUploadIDs(upload_ids)
104+ packageuploadsources = load_referencing(
105+ PackageUploadSource, uploads, ['packageuploadID'])
106 source_file_set = getUtility(ISourcePackageReleaseFileSet)
107 source_files = source_file_set.getByPackageUploadIDs(upload_ids)
108
109+ source_sprs = load_related(
110+ SourcePackageRelease, packageuploadsources,
111+ ['sourcepackagereleaseID'])
112+
113 # Get a dictionary of lists of binary files keyed by upload ID.
114 package_upload_builds_dict = self.builds_dict(
115 upload_ids, binary_files)
116@@ -217,9 +241,11 @@
117 self.old_binary_packages = self.calculateOldBinaries(
118 binary_package_names)
119
120+ package_sets = self.getPackagesetsFor(source_sprs)
121+
122 return [
123 CompletePackageUpload(
124- item, build_upload_files, source_upload_files)
125+ item, build_upload_files, source_upload_files, package_sets)
126 for item in uploads]
127
128 def is_new(self, binarypackagerelease):
129@@ -415,8 +441,9 @@
130 class CompletePackageUpload:
131 """A decorated `PackageUpload` including sources, builds and packages.
132
133- Some properties of PackageUpload are cached here to reduce the number
134- of queries that the +queue template has to make.
135+ This acts effectively as a view for package uploads. Some properties of
136+ the class are cached here to reduce the number of queries that the +queue
137+ template has to make. Others are added here exclusively.
138 """
139 # These need to be predeclared to avoid delegates taking them over.
140 # Would be nice if there was a way of allowing writes to just work
141@@ -433,7 +460,7 @@
142 delegates(IPackageUpload)
143
144 def __init__(self, packageupload, build_upload_files,
145- source_upload_files):
146+ source_upload_files, package_sets):
147 self.pocket = packageupload.pocket
148 self.date_created = packageupload.date_created
149 self.context = packageupload
150@@ -459,6 +486,12 @@
151 else:
152 self.sourcepackagerelease = None
153
154+ if self.contains_source:
155+ self.package_sets = package_sets.get(
156+ self.sourcepackagerelease.sourcepackagenameID, [])
157+ else:
158+ self.package_sets = []
159+
160 @property
161 def pending_delayed_copy(self):
162 """Whether the context is a delayed-copy pending processing."""
163@@ -478,11 +511,87 @@
164 return self.context.changesfile
165
166 @property
167- def package_sets(self):
168- assert self.sourcepackagerelease, \
169- "Can only be used on a source upload."
170- return ' '.join(sorted(ps.name for ps in
171- getUtility(IPackagesetSet).setsIncludingSource(
172- self.sourcepackagerelease.sourcepackagename,
173- distroseries=self.distroseries,
174- direct_inclusion=True)))
175+ def display_package_sets(self):
176+ """Package sets, if any, for display on the +queue page."""
177+ if self.contains_source:
178+ return ' '.join(sorted(
179+ packageset.name for packageset in self.package_sets))
180+ else:
181+ return ""
182+
183+ @property
184+ def display_component(self):
185+ """Component name, if any, for display on the +queue page."""
186+ if self.contains_source:
187+ return self.component_name.lower()
188+ else:
189+ return ""
190+
191+ @property
192+ def display_section(self):
193+ """Section name, if any, for display on the +queue page."""
194+ if self.contains_source:
195+ return self.section_name.lower()
196+ else:
197+ return ""
198+
199+ @property
200+ def display_priority(self):
201+ """Priority name, if any, for display on the +queue page."""
202+ if self.contains_source:
203+ return self.sourcepackagerelease.urgency.name.lower()
204+ else:
205+ return ""
206+
207+ def composeIcon(self, alt, icon, title=None):
208+ """Compose an icon for the package's icon list."""
209+ # These should really be sprites!
210+ if title is None:
211+ title = alt
212+ return '<img alt="[%s]" src="/@@/%s" title="%s" />' % (
213+ cgi.escape(alt, quote=True),
214+ icon,
215+ cgi.escape(title, quote=True),
216+ )
217+
218+ def composeIconList(self):
219+ """List icons that should be shown for this upload."""
220+ ddtp = "Debian Description Translation Project Indexes"
221+ potential_icons = [
222+ (self.contains_source, ("Source", 'package-source')),
223+ (self.contains_build, ("Build", 'package-binary', "Binary")),
224+ (self.contains_translation, ("Translation", 'translation-file')),
225+ (self.contains_installer, ("Installer", 'ubuntu-icon')),
226+ (self.contains_upgrader, ("Upgrader", 'ubuntu-icon')),
227+ (self.contains_ddtp, (ddtp, 'ubuntu-icon')),
228+ ]
229+ return [
230+ self.composeIcon(*details)
231+ for condition, details in potential_icons
232+ if condition]
233+
234+ def composeNameAndChangesLink(self):
235+ """Compose HTML: upload name and link to changes file."""
236+ raw_displayname = self.displayname
237+ displayname = cgi.escape(raw_displayname)
238+ if self.pending_delayed_copy or self.changesfile is None:
239+ return displayname
240+ else:
241+ return '<a href="%s" title="Changes file for %s">%s</a>' % (
242+ self.changesfile.http_url,
243+ cgi.escape(self.displayname, quote=True),
244+ displayname)
245+
246+ @property
247+ def icons_and_name(self):
248+ """Icon list and name, linked to changes file if appropriate."""
249+ iconlist_id = "queue%d-iconlist" % self.id
250+ icons = self.composeIconList()
251+ link = self.composeNameAndChangesLink()
252+ return """
253+ <div id="%s">
254+ %s
255+ %s
256+ (%s)
257+ </div>
258+ """ % (iconlist_id, '\n'.join(icons), link, self.displayarchs)
259
260=== modified file 'lib/lp/soyuz/browser/tests/test_queue.py'
261--- lib/lp/soyuz/browser/tests/test_queue.py 2011-06-16 13:21:14 +0000
262+++ lib/lp/soyuz/browser/tests/test_queue.py 2011-06-22 13:09:13 +0000
263@@ -5,6 +5,8 @@
264
265 __metaclass__ = type
266
267+import cgi
268+from lxml import html
269 import transaction
270 from zope.component import (
271 getUtility,
272@@ -12,8 +14,12 @@
273 )
274
275 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
276-from canonical.testing.layers import LaunchpadFunctionalLayer
277+from canonical.testing.layers import (
278+ LaunchpadFunctionalLayer,
279+ LaunchpadZopelessLayer,
280+ )
281 from lp.archiveuploader.tests import datadir
282+from lp.soyuz.browser.queue import CompletePackageUpload
283 from lp.soyuz.enums import PackageUploadStatus
284 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
285 from lp.soyuz.interfaces.queue import IPackageUploadSet
286@@ -208,8 +214,8 @@
287 upload.distroseries.main_archive)
288 with person_logged_in(queue_admin):
289 view = self.makeView(upload.distroseries, queue_admin)
290- html = view()
291- self.assertIn(upload.package_name, html)
292+ html_text = view()
293+ self.assertIn(upload.package_name, html_text)
294
295 def test_view_renders_build_upload(self):
296 login(ADMIN_EMAIL)
297@@ -218,8 +224,8 @@
298 upload.distroseries.main_archive)
299 with person_logged_in(queue_admin):
300 view = self.makeView(upload.distroseries, queue_admin)
301- html = view()
302- self.assertIn(upload.package_name, html)
303+ html_text = view()
304+ self.assertIn(upload.package_name, html_text)
305
306 def test_view_renders_copy_upload(self):
307 login(ADMIN_EMAIL)
308@@ -228,5 +234,166 @@
309 upload.distroseries.main_archive)
310 with person_logged_in(queue_admin):
311 view = self.makeView(upload.distroseries, queue_admin)
312- html = view()
313- self.assertIn(upload.package_name, html)
314+ html_text = view()
315+ self.assertIn(upload.package_name, html_text)
316+
317+
318+class TestCompletePackageUpload(TestCaseWithFactory):
319+
320+ layer = LaunchpadZopelessLayer
321+
322+ def makeCompletePackageUpload(self, upload=None, build_upload_files=None,
323+ source_upload_files=None,
324+ package_sets=None):
325+ if upload is None:
326+ upload = self.factory.makeSourcePackageUpload()
327+ if build_upload_files is None:
328+ build_upload_files = {}
329+ if source_upload_files is None:
330+ source_upload_files = {}
331+ if package_sets is None:
332+ package_sets = {}
333+ return CompletePackageUpload(
334+ upload, build_upload_files, source_upload_files, package_sets)
335+
336+ def mapPackageSets(self, upload, package_sets=None):
337+ if package_sets is None:
338+ package_sets = [self.factory.makePackageset(
339+ distroseries=upload.distroseries)]
340+ spn = upload.sourcepackagerelease.sourcepackagename
341+ return {spn.id: package_sets}
342+
343+ def test_display_package_sets_returns_source_upload_packagesets(self):
344+ upload = self.factory.makeSourcePackageUpload()
345+ package_sets = self.mapPackageSets(upload)
346+ complete_upload = self.makeCompletePackageUpload(
347+ upload, package_sets=package_sets)
348+ self.assertEqual(
349+ package_sets.values()[0][0].name,
350+ complete_upload.display_package_sets)
351+
352+ def test_display_package_sets_returns_empty_for_non_source_upload(self):
353+ upload = self.factory.makeBuildPackageUpload()
354+ complete_upload = self.makeCompletePackageUpload(
355+ upload, package_sets=self.mapPackageSets(upload))
356+ self.assertEqual("", complete_upload.display_package_sets)
357+
358+ def test_display_package_sets_sorts_by_name(self):
359+ complete_upload = self.makeCompletePackageUpload()
360+ distroseries = complete_upload.distroseries
361+ complete_upload.package_sets = [
362+ self.factory.makePackageset(distroseries=distroseries, name=name)
363+ for name in [u'ccc', u'aaa', u'bbb']]
364+ self.assertEqual("aaa bbb ccc", complete_upload.display_package_sets)
365+
366+ def test_display_component_returns_source_upload_component_name(self):
367+ complete_upload = self.makeCompletePackageUpload()
368+ self.assertEqual(
369+ complete_upload.sourcepackagerelease.component.name.lower(),
370+ complete_upload.display_component)
371+
372+ def test_display_component_returns_empty_for_non_source_upload(self):
373+ complete_upload = self.makeCompletePackageUpload(
374+ self.factory.makeBuildPackageUpload())
375+ self.assertEqual('', complete_upload.display_component)
376+
377+ def test_display_section_returns_source_upload_section_name(self):
378+ complete_upload = self.makeCompletePackageUpload()
379+ self.assertEqual(
380+ complete_upload.sourcepackagerelease.section.name.lower(),
381+ complete_upload.display_section)
382+
383+ def test_display_section_returns_empty_for_non_source_upload(self):
384+ complete_upload = self.makeCompletePackageUpload(
385+ self.factory.makeBuildPackageUpload())
386+ self.assertEqual('', complete_upload.display_section)
387+
388+ def test_display_priority_returns_source_upload_priority(self):
389+ complete_upload = self.makeCompletePackageUpload()
390+ self.assertEqual(
391+ complete_upload.sourcepackagerelease.urgency.name.lower(),
392+ complete_upload.display_priority)
393+
394+ def test_display_priority_returns_empty_for_non_source_upload(self):
395+ complete_upload = self.makeCompletePackageUpload(
396+ self.factory.makeBuildPackageUpload())
397+ self.assertEqual('', complete_upload.display_priority)
398+
399+ def test_composeIcon_produces_image_tag(self):
400+ alt = self.factory.getUniqueString()
401+ icon = self.factory.getUniqueString() + ".png"
402+ title = self.factory.getUniqueString()
403+ html_text = self.makeCompletePackageUpload().composeIcon(
404+ alt, icon, title)
405+ img = html.fromstring(html_text)
406+ self.assertEqual("img", img.tag)
407+ self.assertEqual("[%s]" % alt, img.get("alt"))
408+ self.assertEqual("/@@/" + icon, img.get("src"))
409+ self.assertEqual(title, img.get("title"))
410+
411+ def test_composeIcon_title_defaults_to_alt_text(self):
412+ alt = self.factory.getUniqueString()
413+ icon = self.factory.getUniqueString() + ".png"
414+ html_text = self.makeCompletePackageUpload().composeIcon(alt, icon)
415+ img = html.fromstring(html_text)
416+ self.assertEqual(alt, img.get("title"))
417+
418+ def test_composeIcon_escapes_alt_and_title(self):
419+ alt = 'alt"&'
420+ icon = self.factory.getUniqueString() + ".png"
421+ title = 'title"&'
422+ html_text = self.makeCompletePackageUpload().composeIcon(
423+ alt, icon, title)
424+ img = html.fromstring(html_text)
425+ self.assertEqual("[%s]" % alt, img.get("alt"))
426+ self.assertEqual(title, img.get("title"))
427+
428+ def test_composeIconList_produces_icons(self):
429+ icons = self.makeCompletePackageUpload().composeIconList()
430+ self.assertNotEqual([], icons)
431+ self.assertEqual('img', html.fromstring(icons[0]).tag)
432+
433+ def test_composeIconList_produces_icons_conditionally(self):
434+ complete_upload = self.makeCompletePackageUpload()
435+ base_count = len(complete_upload.composeIconList())
436+ complete_upload.contains_build = True
437+ new_count = len(complete_upload.composeIconList())
438+ self.assertEqual(base_count + 1, new_count)
439+
440+ def test_composeNameAndChangesLink_does_not_link_if_no_changes_file(self):
441+ upload = self.factory.makeCopyJobPackageUpload()
442+ complete_upload = self.makeCompletePackageUpload(upload)
443+ self.assertEqual(
444+ complete_upload.displayname,
445+ complete_upload.composeNameAndChangesLink())
446+
447+ def test_composeNameAndChangesLink_links_to_changes_file(self):
448+ complete_upload = self.makeCompletePackageUpload()
449+ link = html.fromstring(complete_upload.composeNameAndChangesLink())
450+ self.assertEqual(
451+ complete_upload.changesfile.http_url, link.get("href"))
452+
453+ def test_composeNameAndChangesLink_escapes_nonlinked_display_name(self):
454+ filename = 'name"&name'
455+ upload = self.factory.makeCustomPackageUpload(filename=filename)
456+ # Stop nameAndChangesLink from producing a link.
457+ upload.changesfile = None
458+ complete_upload = self.makeCompletePackageUpload(upload)
459+ self.assertIn(
460+ cgi.escape(filename), complete_upload.composeNameAndChangesLink())
461+
462+ def test_composeNameAndChangesLink_escapes_name_in_link(self):
463+ filename = 'name"&name'
464+ upload = self.factory.makeCustomPackageUpload(filename=filename)
465+ complete_upload = self.makeCompletePackageUpload(upload)
466+ link = html.fromstring(complete_upload.composeNameAndChangesLink())
467+ self.assertIn(filename, link.get("title"))
468+ self.assertEqual(filename, link.text)
469+
470+ def test_icons_and_name_composes_icons_and_link_and_archs(self):
471+ complete_upload = self.makeCompletePackageUpload()
472+ icons_and_name = html.fromstring(complete_upload.icons_and_name)
473+ self.assertNotEqual(None, icons_and_name.find("img"))
474+ self.assertNotEqual(None, icons_and_name.find("a"))
475+ self.assertIn(
476+ complete_upload.displayarchs, ' '.join(icons_and_name.itertext()))
477
478=== modified file 'lib/lp/soyuz/interfaces/files.py'
479--- lib/lp/soyuz/interfaces/files.py 2010-08-20 20:31:18 +0000
480+++ lib/lp/soyuz/interfaces/files.py 2011-06-22 13:09:13 +0000
481@@ -14,6 +14,7 @@
482 'ISourcePackageReleaseFileSet',
483 ]
484
485+from lazr.restful.fields import Reference
486 from zope.interface import Interface
487 from zope.schema import (
488 Bool,
489@@ -21,6 +22,7 @@
490 )
491
492 from canonical.launchpad import _
493+from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
494
495
496 class IBinaryPackageFile(Interface):
497@@ -57,11 +59,16 @@
498 id = Int(
499 title=_('ID'), required=True, readonly=True,
500 )
501- sourcepackagerelease = Int(
502- title=_('The sourcepackagerelease being published'),
503+
504+ sourcepackagerelease = Reference(
505+ ISourcePackageRelease,
506+ title=_("The source package release being published"),
507+ required=True, readonly=False)
508+
509+ sourcepackagereleaseID = Int(
510+ title=_('ID of the source package release being published'),
511 required=True,
512- readonly=False,
513- )
514+ readonly=False)
515
516 libraryfile = Int(
517 title=_('The library file alias for this file'), required=True,
518
519=== modified file 'lib/lp/soyuz/interfaces/packageset.py'
520--- lib/lp/soyuz/interfaces/packageset.py 2011-06-16 18:51:45 +0000
521+++ lib/lp/soyuz/interfaces/packageset.py 2011-06-22 13:09:13 +0000
522@@ -445,6 +445,17 @@
523 :return: An iterable collection of `IPackageset` instances.
524 """
525
526+ def getForPackages(distroseries, sourcepackagename_ids):
527+ """Get `Packagesets` that directly contain the given packages.
528+
529+ :param distroseries: `DistroSeries` to look in. Only packagesets for
530+ this series will be returned.
531+ :param sourcepackagename_ids: A sequence of `SourcePackageName` ids.
532+ Only packagesets for these package names will be returned.
533+ :return: A dict mapping `SourcePackageName` ids to lists of their
534+ respective packagesets, in no particular order.
535+ """
536+
537 @operation_parameters(
538 sourcepackagename=TextLine(
539 title=_('Source package name'), required=True),
540
541=== modified file 'lib/lp/soyuz/model/packageset.py'
542--- lib/lp/soyuz/model/packageset.py 2011-03-22 11:30:13 +0000
543+++ lib/lp/soyuz/model/packageset.py 2011-06-22 13:09:13 +0000
544@@ -1,4 +1,4 @@
545-# Copyright 2009 Canonical Ltd. This software is licensed under the
546+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
547 # GNU Affero General Public License version 3 (see the file LICENSE).
548
549 __metaclass__ = type
550@@ -36,6 +36,7 @@
551 NoSuchPackageSet,
552 )
553 from lp.soyuz.model.packagesetgroup import PackagesetGroup
554+from lp.soyuz.model.packagesetsources import PackagesetSources
555
556
557 def _order_result_set(result_set):
558@@ -427,23 +428,36 @@
559 source_name = getUtility(ISourcePackageNameSet)[source_name]
560 return source_name
561
562+ def getForPackages(self, distroseries, sourcepackagename_ids):
563+ """See `IPackagesetSet`."""
564+ tuples = IStore(Packageset).find(
565+ (PackagesetSources.sourcepackagename_id, Packageset),
566+ Packageset.id == PackagesetSources.packageset_id,
567+ Packageset.distroseries == distroseries,
568+ PackagesetSources.sourcepackagename_id.is_in(
569+ sourcepackagename_ids))
570+ packagesets_by_package = {}
571+ for package, packageset in tuples:
572+ packagesets_by_package.setdefault(package, []).append(packageset)
573+ return packagesets_by_package
574+
575 def setsIncludingSource(self, sourcepackagename, distroseries=None,
576 direct_inclusion=False):
577 """See `IPackagesetSet`."""
578 sourcepackagename = self._nameToSourcePackageName(sourcepackagename)
579
580- if direct_inclusion == False:
581+ if direct_inclusion:
582+ query = '''
583+ SELECT pss.packageset FROM packagesetsources pss
584+ WHERE pss.sourcepackagename = ?
585+ '''
586+ else:
587 query = '''
588 SELECT fpsi.parent
589 FROM packagesetsources pss, flatpackagesetinclusion fpsi
590 WHERE pss.sourcepackagename = ?
591 AND pss.packageset = fpsi.child
592 '''
593- else:
594- query = '''
595- SELECT pss.packageset FROM packagesetsources pss
596- WHERE pss.sourcepackagename = ?
597- '''
598 store = IStore(Packageset)
599 psets = SQL(query, (sourcepackagename.id,))
600 clauses = [Packageset.id.is_in(psets)]
601
602=== added file 'lib/lp/soyuz/model/packagesetsources.py'
603--- lib/lp/soyuz/model/packagesetsources.py 1970-01-01 00:00:00 +0000
604+++ lib/lp/soyuz/model/packagesetsources.py 2011-06-22 13:09:13 +0000
605@@ -0,0 +1,41 @@
606+# Copyright 2011 Canonical Ltd. This software is licensed under the
607+# GNU Affero General Public License version 3 (see the file LICENSE).
608+
609+"""The `PackagesetSources` linking table.
610+
611+This table associates `Packageset`s with `SourcePackageName`s.
612+"""
613+
614+__metaclass__ = type
615+__all__ = [
616+ 'PackagesetSources',
617+ ]
618+
619+from storm.locals import (
620+ Int,
621+ Reference,
622+ Storm,
623+ )
624+
625+
626+class PackagesetSources(Storm):
627+ """Linking table: which packages are in a package set?"""
628+ # This table is largely managed from Packageset, but also directly
629+ # accessed from other places.
630+
631+ __storm_table__ = 'PackagesetSources'
632+
633+ # There's a vestigial id as well, a holdover from the SQLObject
634+ # days. Nobody seems to use it. The only key that matters is
635+ # (packageset, sourcepackagename).
636+ # XXX JeroenVermeulen 2011-06-22, bug=800677: Drop the id column.
637+ __storm_primary__ = (
638+ 'packageset_id',
639+ 'sourcepackagename_id',
640+ )
641+
642+ packageset_id = Int(name='packageset')
643+ packageset = Reference(packageset_id, 'Packageset.id')
644+ sourcepackagename_id = Int(name='sourcepackagename')
645+ sourcepackagename = Reference(
646+ sourcepackagename_id, 'SourcePackageName.id')
647
648=== modified file 'lib/lp/soyuz/templates/distroseries-queue.pt'
649--- lib/lp/soyuz/templates/distroseries-queue.pt 2011-06-16 13:21:14 +0000
650+++ lib/lp/soyuz/templates/distroseries-queue.pt 2011-06-22 13:09:13 +0000
651@@ -89,33 +89,30 @@
652 <input type="checkbox" name="QUEUE_ID"
653 tal:attributes="value packageupload/id"/>
654 </td>
655- <td style="padding-top: 5px">
656- <metal:iconlist use-macro="template/macros/package-iconlist"/>
657+ <td
658+ style="padding-top: 5px"
659+ tal:content="structure packageupload/icons_and_name">
660 </td>
661
662 <td style="padding-top: 5px"
663 tal:content="packageupload/displayversion">2.0.17-6
664 </td>
665 <tal:is_source define="is_source packageupload/contains_source">
666- <td style="padding-top: 5px">
667- <tal:component condition="is_source"
668- content="packageupload/sourcepackagerelease/component/name/lower">
669- </tal:component>
670- </td>
671- <td style="padding-top: 5px">
672- <tal:section condition="is_source"
673- content="packageupload/sourcepackagerelease/section/name/lower">
674- </tal:section>
675- </td>
676- <td style="padding-top: 5px">
677- <tal:priority condition="is_source"
678- content="packageupload/sourcepackagerelease/urgency/name/lower">
679- </tal:priority>
680- </td>
681- <td style="padding-top: 5px">
682- <tal:packagesets condition="is_source"
683- content="packageupload/package_sets">
684- </tal:packagesets>
685+ <td
686+ style="padding-top: 5px"
687+ tal:content="packageupload/display_component">
688+ </td>
689+ <td
690+ style="padding-top: 5px"
691+ tal:content="packageupload/display_section">
692+ </td>
693+ <td
694+ style="padding-top: 5px"
695+ tal:content="packageupload/display_priority">
696+ </td>
697+ <td
698+ style="padding-top: 5px"
699+ tal:content="packageupload/display_package_sets">
700 </td>
701 </tal:is_source>
702 <td style="padding-top: 5px"
703@@ -232,7 +229,9 @@
704 style="display:none">
705 <td/>
706 <td tal:condition="view/availableActions"/>
707- <td tal:define="libraryfilealias file/libraryfile">
708+ <td
709+ tal:define="libraryfilealias file/libraryfile"
710+ tal:condition="libraryfilealias">
711 <metal:file use-macro="template/macros/package-file"/>
712 </td>
713 <td colspan="6"/>
714@@ -298,59 +297,15 @@
715 :libraryfilealias: A LibraryFileAlias to link to. If it is expired,
716 no link will be created.
717 </tal:comment>
718- <tal:not-none condition="libraryfilealias">
719- <tal:unexpired tal:condition="libraryfilealias/content">
720- <a tal:attributes="href libraryfilealias/http_url">
721- <tal:filename replace="libraryfilealias/filename"/>
722- </a>
723- (<span tal:replace="libraryfilealias/content/filesize/fmt:bytes" />)
724- </tal:unexpired>
725- <tal:expired tal:condition="not:libraryfilealias/content">
726- <span tal:content="libraryfilealias/filename"/>
727- </tal:expired>
728- </tal:not-none>
729-</metal:macro>
730-
731-<metal:macro define-macro="package-iconlist">
732- <tal:comment replace="nothing">
733- This macro expects the following variables defined:
734- :packageupload: A PackageUpload record for which we display icons.
735- </tal:comment>
736-
737- <div tal:attributes="id string:queue${packageupload/id}-iconlist">
738- <img tal:condition="packageupload/contains_source"
739- alt="[Source]" src="/@@/package-source" title="Source"/>
740- <img tal:condition="packageupload/contains_build"
741- alt="[Build]" src="/@@/package-binary" title="Binary"/>
742- <img tal:condition="packageupload/contains_translation"
743- alt="[Translation]" src="/@@/translation-file"
744- title="Translation"/>
745- <img tal:condition="packageupload/contains_installer"
746- alt="[Installer]" src="/@@/ubuntu-icon"
747- title="Installer"/>
748- <img tal:condition="packageupload/contains_upgrader"
749- alt="[Upgrader]" src="/@@/ubuntu-icon"
750- title="Upgrader"/>
751- <img tal:condition="packageupload/contains_ddtp"
752- alt="[Debian Description Translation Project Indexes]"
753- src="/@@/ubuntu-icon"
754- title="Debian Description Translation Project Indexes"/>
755- <tal:not-delayed condition="not: packageupload/pending_delayed_copy">
756- <a tal:condition="packageupload/changesfile"
757- tal:content="packageupload/displayname"
758- tal:attributes="
759- href packageupload/changesfile/http_url;
760- title string:Changes file for ${packageupload/displayname};">
761- </a>
762- <tal:no-changes-file
763- condition="not: packageupload/changesfile"
764- replace="packageupload/displayname"/>
765- </tal:not-delayed>
766- <tal:pending_delayed_copy_title
767- condition="packageupload/pending_delayed_copy"
768- replace="packageupload/displayname" />
769- <tal:arches replace="string: (${packageupload/displayarchs})"/>
770- </div>
771+ <tal:unexpired tal:condition="libraryfilealias/content">
772+ <a tal:attributes="href libraryfilealias/http_url">
773+ <tal:filename replace="libraryfilealias/filename"/>
774+ </a>
775+ (<span tal:replace="libraryfilealias/content/filesize/fmt:bytes" />)
776+ </tal:unexpired>
777+ <tal:expired tal:condition="not:libraryfilealias/content">
778+ <span tal:content="libraryfilealias/filename"/>
779+ </tal:expired>
780 </metal:macro>
781
782 </metal:macros>
783
784=== modified file 'lib/lp/soyuz/tests/test_packageset.py'
785--- lib/lp/soyuz/tests/test_packageset.py 2011-03-22 11:30:13 +0000
786+++ lib/lp/soyuz/tests/test_packageset.py 2011-06-22 13:09:13 +0000
787@@ -1,11 +1,11 @@
788-# Copyright 2009 Canonical Ltd. This software is licensed under the
789+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
790 # GNU Affero General Public License version 3 (see the file LICENSE).
791
792 """Test Packageset features."""
793
794 from zope.component import getUtility
795
796-from canonical.testing.layers import LaunchpadZopelessLayer
797+from canonical.testing.layers import ZopelessDatabaseLayer
798 from lp.registry.interfaces.distribution import IDistributionSet
799 from lp.registry.interfaces.series import SeriesStatus
800 from lp.soyuz.interfaces.packageset import (
801@@ -17,128 +17,154 @@
802
803 class TestPackagesetSet(TestCaseWithFactory):
804
805- layer = LaunchpadZopelessLayer
806-
807- def setUp(self):
808- """Setup a distribution with multiple distroseries."""
809- super(TestPackagesetSet, self).setUp()
810- self.distribution = getUtility(IDistributionSet).getByName(
811- 'ubuntu')
812- self.distroseries_current = self.distribution.currentseries
813- self.distroseries_experimental = self.factory.makeDistroRelease(
814- distribution = self.distribution, name="experimental",
815+ layer = ZopelessDatabaseLayer
816+
817+ def getUbuntu(self):
818+ """Get the Ubuntu `Distribution`."""
819+ return getUtility(IDistributionSet).getByName('ubuntu')
820+
821+ def makeExperimentalSeries(self):
822+ """Create an experimental Ubuntu `DistroSeries`."""
823+ return self.factory.makeDistroSeries(
824+ distribution=self.getUbuntu(), name="experimental",
825 status=SeriesStatus.EXPERIMENTAL)
826
827- self.person1 = self.factory.makePerson(
828- name='hacker', displayname=u'Happy Hacker')
829-
830- self.packageset_set = getUtility(IPackagesetSet)
831-
832 def test_new_defaults_to_current_distroseries(self):
833 # If the distroseries is not provided, the current development
834 # distroseries will be assumed.
835- packageset = self.packageset_set.new(
836- u'kernel', u'Contains all OS kernel packages', self.person1)
837-
838+ packageset = getUtility(IPackagesetSet).new(
839+ self.factory.getUniqueUnicode(), self.factory.getUniqueUnicode(),
840+ self.factory.makePerson())
841 self.failUnlessEqual(
842- self.distroseries_current, packageset.distroseries)
843+ self.getUbuntu().currentseries, packageset.distroseries)
844
845 def test_new_with_specified_distroseries(self):
846 # A distroseries can be provided when creating a package set.
847- packageset = self.packageset_set.new(
848- u'kernel', u'Contains all OS kernel packages', self.person1,
849- distroseries=self.distroseries_experimental)
850-
851- self.failUnlessEqual(
852- self.distroseries_experimental, packageset.distroseries)
853+ experimental_series = self.makeExperimentalSeries()
854+ packageset = getUtility(IPackagesetSet).new(
855+ self.factory.getUniqueUnicode(), self.factory.getUniqueUnicode(),
856+ self.factory.makePerson(), distroseries=experimental_series)
857+ self.failUnlessEqual(experimental_series, packageset.distroseries)
858
859 def test_new_creates_new_packageset_group(self):
860 # Creating a new packageset should also create a new packageset
861 # group with the same owner.
862- packageset = self.packageset_set.new(
863- u'kernel', u'Contains all OS kernel packages', self.person1,
864- distroseries=self.distroseries_experimental)
865-
866- self.failUnlessEqual(
867- self.person1, packageset.packagesetgroup.owner)
868+ owner = self.factory.makePerson()
869+ experimental_series = self.makeExperimentalSeries()
870+ packageset = getUtility(IPackagesetSet).new(
871+ self.factory.getUniqueUnicode(), self.factory.getUniqueUnicode(),
872+ owner, distroseries=experimental_series)
873+ self.failUnlessEqual(owner, packageset.packagesetgroup.owner)
874
875 def test_new_duplicate_name_for_same_distroseries(self):
876 # Creating a packageset with a duplicate name for the
877 # given distroseries will fail.
878- packageset = self.packageset_set.new(
879- u'kernel', u'Contains all OS kernel packages', self.person1,
880- distroseries=self.distroseries_experimental)
881-
882- self.failUnlessRaises(
883- DuplicatePackagesetName, self.packageset_set.new,
884- u'kernel', u'A packageset with a duplicate name', self.person1,
885- distroseries=self.distroseries_experimental)
886+ distroseries = self.factory.makeDistroSeries()
887+ name = self.factory.getUniqueUnicode()
888+ self.factory.makePackageset(name, distroseries=distroseries)
889+ self.assertRaises(
890+ DuplicatePackagesetName, getUtility(IPackagesetSet).new,
891+ name, self.factory.getUniqueUnicode(), self.factory.makePerson(),
892+ distroseries=distroseries)
893
894 def test_new_duplicate_name_for_different_distroseries(self):
895 # Creating a packageset with a duplicate name but for a different
896 # series is no problem.
897- packageset = self.packageset_set.new(
898- u'kernel', u'Contains all OS kernel packages', self.person1)
899-
900- packageset2 = self.packageset_set.new(
901- u'kernel', u'A packageset with a duplicate name', self.person1,
902- distroseries=self.distroseries_experimental)
903- self.assertEqual(packageset.name, packageset2.name)
904+ name = self.factory.getUniqueUnicode()
905+ packageset1 = self.factory.makePackageset(name)
906+ packageset2 = getUtility(IPackagesetSet).new(
907+ name, self.factory.getUniqueUnicode(), self.factory.makePerson(),
908+ distroseries=self.factory.makeDistroSeries())
909+ self.assertEqual(packageset1.name, packageset2.name)
910
911 def test_new_related_packageset(self):
912 # Creating a new package set while specifying a `related_set` should
913 # have the effect that the former ends up in the same group as the
914 # latter.
915- pset1 = self.packageset_set.new(
916- u'kernel', u'Contains all OS kernel packages', self.person1)
917-
918- pset2 = self.packageset_set.new(
919- u'kernel', u'A related package set.', self.person1,
920- distroseries=self.distroseries_experimental, related_set=pset1)
921+ name = self.factory.getUniqueUnicode()
922+ pset1 = self.factory.makePackageset(name)
923+ pset2 = self.factory.makePackageset(
924+ name, distroseries=self.makeExperimentalSeries(),
925+ related_set=pset1)
926 self.assertEqual(pset1.packagesetgroup, pset2.packagesetgroup)
927
928 def test_get_by_name_in_current_distroseries(self):
929 # IPackagesetSet.getByName() will return the package set in the
930 # current distroseries if the optional `distroseries` parameter is
931 # omitted.
932- pset1 = self.packageset_set.new(
933- u'kernel', u'Contains all OS kernel packages', self.person1)
934- pset2 = self.packageset_set.new(
935- u'kernel', u'A related package set.', self.person1,
936- distroseries=self.distroseries_experimental, related_set=pset1)
937- pset_found = getUtility(IPackagesetSet).getByName('kernel')
938- self.assertEqual(pset1, pset_found)
939+ name = self.factory.getUniqueUnicode()
940+ pset1 = self.factory.makePackageset(name)
941+ self.factory.makePackageset(
942+ name, distroseries=self.makeExperimentalSeries(),
943+ related_set=pset1)
944+ self.assertEqual(pset1, getUtility(IPackagesetSet).getByName(name))
945
946 def test_get_by_name_in_specified_distroseries(self):
947 # IPackagesetSet.getByName() will return the package set in the
948 # specified distroseries.
949- pset1 = self.packageset_set.new(
950- u'kernel', u'Contains all OS kernel packages', self.person1)
951- pset2 = self.packageset_set.new(
952- u'kernel', u'A related package set.', self.person1,
953- distroseries=self.distroseries_experimental, related_set=pset1)
954+ name = self.factory.getUniqueUnicode()
955+ experimental_series = self.makeExperimentalSeries()
956+ pset1 = self.factory.makePackageset(name)
957+ pset2 = self.factory.makePackageset(
958+ name, distroseries=experimental_series, related_set=pset1)
959 pset_found = getUtility(IPackagesetSet).getByName(
960- 'kernel', distroseries=self.distroseries_experimental)
961+ name, distroseries=experimental_series)
962 self.assertEqual(pset2, pset_found)
963
964 def test_get_by_distroseries(self):
965 # IPackagesetSet.getBySeries() will return those package sets
966 # associated with the given distroseries.
967- pset1 = self.packageset_set.new(
968- u'timmy', u'Timmy Mallett', self.person1)
969- pset2 = self.packageset_set.new(
970- u'savile', u'Jimmy Savile', self.person1)
971- self.packageset_set.new(
972- u'hoskins', u'Bob Hoskins', self.person1,
973- distroseries=self.distroseries_experimental)
974+ package_sets_for_current_ubuntu = [
975+ self.factory.makePackageset() for counter in xrange(2)]
976+ self.factory.makePackageset(
977+ distroseries=self.makeExperimentalSeries())
978 self.assertContentEqual(
979- [pset1, pset2],
980- self.packageset_set.getBySeries(self.distroseries_current))
981+ package_sets_for_current_ubuntu,
982+ getUtility(IPackagesetSet).getBySeries(
983+ self.getUbuntu().currentseries))
984+
985+ def test_getForPackages_returns_packagesets(self):
986+ # getForPackages finds package sets for given source package
987+ # names in a distroseries, and maps them by
988+ # SourcePackageName.id.
989+ series = self.factory.makeDistroSeries()
990+ packageset = self.factory.makePackageset(distroseries=series)
991+ package = self.factory.makeSourcePackageName()
992+ packageset.addSources([package.name])
993+ self.assertEqual(
994+ {package.id: [packageset]},
995+ getUtility(IPackagesetSet).getForPackages(series, [package.id]))
996+
997+ def test_getForPackages_filters_by_distroseries(self):
998+ # getForPackages does not return packagesets for different
999+ # distroseries.
1000+ series = self.factory.makeDistroSeries()
1001+ other_series = self.factory.makeDistroSeries()
1002+ packageset = self.factory.makePackageset(distroseries=series)
1003+ package = self.factory.makeSourcePackageName()
1004+ packageset.addSources([package.name])
1005+ self.assertEqual(
1006+ {},
1007+ getUtility(IPackagesetSet).getForPackages(
1008+ other_series, [package.id]))
1009+
1010+ def test_getForPackages_filters_by_sourcepackagename(self):
1011+ # getForPackages does not return packagesets for different
1012+ # source package names.
1013+ series = self.factory.makeDistroSeries()
1014+ packageset = self.factory.makePackageset(distroseries=series)
1015+ package = self.factory.makeSourcePackageName()
1016+ other_package = self.factory.makeSourcePackageName()
1017+ packageset.addSources([package.name])
1018+ self.assertEqual(
1019+ {},
1020+ getUtility(IPackagesetSet).getForPackages(
1021+ series, [other_package.id]))
1022
1023
1024 class TestPackageset(TestCaseWithFactory):
1025
1026- layer = LaunchpadZopelessLayer
1027+ layer = ZopelessDatabaseLayer
1028
1029 def setUp(self):
1030 """Setup a distribution with multiple distroseries."""
1031@@ -147,10 +173,10 @@
1032 'ubuntu')
1033 self.distroseries_current = self.distribution.currentseries
1034 self.distroseries_experimental = self.factory.makeDistroRelease(
1035- distribution = self.distribution, name="experimental",
1036+ distribution=self.distribution, name="experimental",
1037 status=SeriesStatus.EXPERIMENTAL)
1038 self.distroseries_experimental2 = self.factory.makeDistroRelease(
1039- distribution = self.distribution, name="experimental2",
1040+ distribution=self.distribution, name="experimental2",
1041 status=SeriesStatus.EXPERIMENTAL)
1042
1043 self.person1 = self.factory.makePerson(