Merge lp:~jtv/launchpad/bug-798521 into lp:launchpad
- bug-798521
- Merge into devel
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 |
Related bugs: |
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 SourcePackageRe
Batch-fetched any Packagesets associated with the SourcePackages related to SourcePackageRe
Extended CompletePackage
Moved some small-scale TAL logic into CompletePackage
Unit-tested CompletePackage
Modernized the PackagesetSet unit test, replacing its setUp with specific factory calls.
== Tests ==
{{{
./bin/test -vvc soyuz.browser.
./bin/test -vvc soyuz.tests.
./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/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
Jeroen T. Vermeulen (jtv) wrote : | # |
Thanks for the review!
> > === modified file 'lib/lp/
> > @@ -176,6 +182,15 @@
> > # Listify to avoid repeated queries.
> > return list(old_
> >
> > + def getPackagesetsF
> > + """Find associated `Packagesets`.
> > +
> > + :param source_
> `SourcePackageR
> > + """
> > + sprs = [spr for spr in source_
> None]
> > + return getUtility(
> > + self.context, set(spr.
> 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_
> >
> > + package_sets = self.getPackage
>
> 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 CompletePackage
> > """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.
> >
> > @property
> > - def package_sets(self):
> > - assert self.sourcepack
> > - "Can only be used on a source upload."
> > - return ' '.join(
> > - getUtility(
> > - self.sourcepack
> > - distroseries=
> > - direct_
>
> Is self.contains_
> 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...
Preview Diff
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( |
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' binary_ packages) or(self, source_ package_ releases) : package_ releases: A sequence of elease` s. package_ releases if spr is not IPackagesetSet) .getForPackages ( sourcepackagena meID for spr in
> @@ -176,6 +182,15 @@
> # Listify to avoid repeated queries.
> return list(old_
>
> + def getPackagesetsF
> + """Find associated `Packagesets`.
> +
> + :param source_
`SourcePackageR
> + """
> + sprs = [spr for spr in source_
None]
> + return getUtility(
> + self.context, set(spr.
sprs))
Any reason you are not constructing a set directly in the line above?
> + packageuploadso urces = load_referencing(
> + source_sprs = load_related(
Didn't know about these, cool stuff :)
> @@ -217,9 +241,11 @@ binary_ packages = self.calculateO ldBinaries( package_ names) setsFor( source_ sprs)
> self.old_
> binary_
>
> + package_sets = self.getPackage
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 @@ Upload:
> class CompletePackage
> """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 @@ agerelease = None source: agerelease. sourcepackagena meID, []) delayed_ copy(self) : changesfile agerelease, \ sorted( ps.name for ps in IPackagesetSet) .setsIncludingS ource( agerelease. sourcepackagena me, self.distroseri es, inclusion= True)))
> else:
> self.sourcepack
>
> + if self.contains_
> + self.package_sets = package_sets.get(
> + self.sourcepack
> + else:
> + self.package_sets = []
> +
> @property
> def pending_
> """Whether the context is a delayed-copy pending
processing."""
> @@ -478,11 +511,87 @@
> return self.context.
>
> @property
> - def package_sets(self):
> - assert self.sourcepack
> - "Can only be used on a source upload."
> - return ' '.join(
> - getUtility(
> - self.sourcepack
> - distroseries=
> - direct_
Is self.contains_ source equival...