Merge lp:~cjwatson/launchpad/distribution-filebug-dsp-vocab into lp:launchpad
- distribution-filebug-dsp-vocab
- Merge into devel
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 18194 | ||||||||
Proposed branch: | lp:~cjwatson/launchpad/distribution-filebug-dsp-vocab | ||||||||
Merge into: | lp:launchpad | ||||||||
Diff against target: |
750 lines (+363/-75) 9 files modified
database/sampledata/current-dev.sql (+1/-1) database/sampledata/current.sql (+1/-1) lib/lp/bugs/browser/bugtarget.py (+48/-20) lib/lp/bugs/browser/tests/test_bugtarget_filebug.py (+22/-2) lib/lp/bugs/browser/widgets/bugtask.py (+46/-5) lib/lp/bugs/doc/bugtask-package-widget.txt (+128/-23) lib/lp/bugs/tests/test_doc.py (+26/-1) lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py (+52/-3) lib/lp/registry/vocabularies.py (+39/-19) |
||||||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/distribution-filebug-dsp-vocab | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+305150@code.launchpad.net |
Commit message
Convert Distribution:
Description of the change
Convert Distribution:
The hardest bit of this was revealed by trying to extend an existing doctest to test this case: there are not entirely unreasonable use cases for adding bug tasks to existing bugs for packages in distributions whose set of packages we don't track at all, for example "this bug also affects the 'unzip' package in Gentoo and here's their bug on the subject". I think it's overkill to forbid this altogether, but we can and should be strict about distributions whose packages we track properly, and we should still not allow searching for SPNs even via other distributions. The compromise I found was to allow DistributionSou
William Grant (wgrant) : | # |
Preview Diff
1 | === modified file 'database/sampledata/current-dev.sql' |
2 | --- database/sampledata/current-dev.sql 2016-07-22 11:23:31 +0000 |
3 | +++ database/sampledata/current-dev.sql 2016-09-19 12:53:26 +0000 |
4 | @@ -3500,7 +3500,7 @@ |
5 | INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (6, 1, 19, 'alsa-utils', '', '', '', NULL, NULL, 1); |
6 | INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (7, 1, 20, 'cnews', '', '', '', NULL, NULL, 1); |
7 | INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (8, 1, 21, 'libstdc++', '', '', '', NULL, NULL, 1); |
8 | -INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (9, 1, 22, 'linux-source-2.6.15', '', '', '', NULL, NULL, 1); |
9 | +INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (9, 1, 22, 'linux-source-2.6.15', 'linux-2.6.12', 'the kernel of boom', 'this kernel is like the crystal method: a temple of boom', NULL, NULL, 1); |
10 | INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (10, 1, 23, 'foobar', '', '', '', NULL, NULL, 1); |
11 | INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (11, 1, 27, 'commercialpackage', '', '', '', NULL, NULL, 12); |
12 | |
13 | |
14 | === modified file 'database/sampledata/current.sql' |
15 | --- database/sampledata/current.sql 2016-07-22 10:48:21 +0000 |
16 | +++ database/sampledata/current.sql 2016-09-19 12:53:26 +0000 |
17 | @@ -3434,7 +3434,7 @@ |
18 | INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (6, 1, 19, 'alsa-utils', '', '', '', NULL, NULL, 1); |
19 | INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (7, 1, 20, 'cnews', '', '', '', NULL, NULL, 1); |
20 | INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (8, 1, 21, 'libstdc++', '', '', '', NULL, NULL, 1); |
21 | -INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (9, 1, 22, 'linux-source-2.6.15', '', '', '', NULL, NULL, 1); |
22 | +INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (9, 1, 22, 'linux-source-2.6.15', 'linux-2.6.12', 'the kernel of boom', 'this kernel is like the crystal method: a temple of boom', NULL, NULL, 1); |
23 | INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (10, 1, 23, 'foobar', '', '', '', NULL, NULL, 1); |
24 | INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (11, 1, 27, 'commercialpackage', '', '', '', NULL, NULL, 12); |
25 | |
26 | |
27 | === modified file 'lib/lp/bugs/browser/bugtarget.py' |
28 | --- lib/lp/bugs/browser/bugtarget.py 2016-04-29 11:11:35 +0000 |
29 | +++ lib/lp/bugs/browser/bugtarget.py 2016-09-19 12:53:26 +0000 |
30 | @@ -91,6 +91,7 @@ |
31 | BugTagsWidget, |
32 | LargeBugTagsWidget, |
33 | ) |
34 | +from lp.bugs.browser.widgets.bugtask import FileBugSourcePackageNameWidget |
35 | from lp.bugs.interfaces.apportjob import IProcessApportBlobJobSource |
36 | from lp.bugs.interfaces.bug import ( |
37 | CreateBugParams, |
38 | @@ -131,6 +132,7 @@ |
39 | from lp.registry.interfaces.sourcepackage import ISourcePackage |
40 | from lp.registry.vocabularies import ValidPersonOrTeamVocabulary |
41 | from lp.services.config import config |
42 | +from lp.services.features import getFeatureFlag |
43 | from lp.services.job.interfaces.job import JobStatus |
44 | from lp.services.librarian.browser import ProxiedLibraryFileAlias |
45 | from lp.services.propertycache import cachedproperty |
46 | @@ -225,6 +227,7 @@ |
47 | |
48 | custom_widget('information_type', LaunchpadRadioWidgetWithDescription) |
49 | custom_widget('comment', TextAreaWidget, cssClass='comment-text') |
50 | + custom_widget('packagename', FileBugSourcePackageNameWidget) |
51 | |
52 | extra_data_token = None |
53 | |
54 | @@ -419,8 +422,17 @@ |
55 | distribution = self.context.distribution |
56 | |
57 | try: |
58 | - distribution.guessPublishedSourcePackageName(packagename) |
59 | - except NotFoundError: |
60 | + if bool(getFeatureFlag('disclosure.dsp_picker.enabled')): |
61 | + dsp_vocab = self.widgets.get("packagename").vocabulary |
62 | + dsp_vocab.setDistribution(distribution) |
63 | + dsp_vocab.getTermByToken(packagename) |
64 | + else: |
65 | + # The untrusted BinaryAndSourcePackageName |
66 | + # vocabulary was used, so it needs secondary |
67 | + # verification. |
68 | + distribution.guessPublishedSourcePackageName( |
69 | + packagename) |
70 | + except (LookupError, NotFoundError): |
71 | if distribution.series: |
72 | # If a distribution doesn't have any series, |
73 | # it won't have any source packages published at |
74 | @@ -525,24 +537,28 @@ |
75 | information_type=information_type, |
76 | tags=data.get('tags')) |
77 | if IDistribution.providedBy(context) and packagename: |
78 | - # We don't know if the package name we got was a source or binary |
79 | - # package name, so let the Soyuz API figure it out for us. |
80 | - packagename = str(packagename.name) |
81 | - try: |
82 | - sourcepackagename = context.guessPublishedSourcePackageName( |
83 | - packagename) |
84 | - except NotFoundError: |
85 | - notifications.append( |
86 | - "The package %s is not published in %s; the " |
87 | - "bug was targeted only to the distribution." |
88 | - % (packagename, context.displayname)) |
89 | - params.comment += ( |
90 | - "\r\n\r\nNote: the original reporter indicated " |
91 | - "the bug was in package %r; however, that package " |
92 | - "was not published in %s." % ( |
93 | - packagename, context.displayname)) |
94 | + if bool(getFeatureFlag('disclosure.dsp_picker.enabled')): |
95 | + context = packagename |
96 | else: |
97 | - context = context.getSourcePackage(sourcepackagename.name) |
98 | + # We don't know if the package name we got was a source or |
99 | + # binary package name, so let the Soyuz API figure it out |
100 | + # for us. |
101 | + packagename = str(packagename.name) |
102 | + try: |
103 | + sourcepackagename = ( |
104 | + context.guessPublishedSourcePackageName(packagename)) |
105 | + except NotFoundError: |
106 | + notifications.append( |
107 | + "The package %s is not published in %s; the " |
108 | + "bug was targeted only to the distribution." |
109 | + % (packagename, context.displayname)) |
110 | + params.comment += ( |
111 | + "\r\n\r\nNote: the original reporter indicated " |
112 | + "the bug was in package %r; however, that package " |
113 | + "was not published in %s." % ( |
114 | + packagename, context.displayname)) |
115 | + else: |
116 | + context = context.getSourcePackage(sourcepackagename.name) |
117 | |
118 | extra_data = self.extra_data |
119 | if extra_data.extra_description: |
120 | @@ -895,13 +911,25 @@ |
121 | filebug_url, status=httplib.MOVED_PERMANENTLY) |
122 | |
123 | |
124 | +class IDistroBugAddForm(IBugAddForm): |
125 | + |
126 | + packagename = copy_field( |
127 | + IBugAddForm['packagename'], vocabularyName='DistributionSourcePackage') |
128 | + |
129 | + |
130 | class FilebugShowSimilarBugsView(FileBugViewBase): |
131 | """A view for showing possible dupes for a bug. |
132 | |
133 | This view will only be used to populate asynchronously-driven parts |
134 | of a page. |
135 | """ |
136 | - schema = IBugAddForm |
137 | + |
138 | + @property |
139 | + def schema(self): |
140 | + if bool(getFeatureFlag('disclosure.dsp_picker.enabled')): |
141 | + return IDistroBugAddForm |
142 | + else: |
143 | + return IBugAddForm |
144 | |
145 | # XXX: Brad Bollenbach 2006-10-04: This assignment to actions is a |
146 | # hack to make the action decorator Just Work across inheritance. |
147 | |
148 | === modified file 'lib/lp/bugs/browser/tests/test_bugtarget_filebug.py' |
149 | --- lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2016-01-26 15:47:37 +0000 |
150 | +++ lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2016-09-19 12:53:26 +0000 |
151 | @@ -1,4 +1,4 @@ |
152 | -# Copyright 2010-2012 Canonical Ltd. This software is licensed under the |
153 | +# Copyright 2010-2016 Canonical Ltd. This software is licensed under the |
154 | # GNU Affero General Public License version 3 (see the file LICENSE). |
155 | |
156 | __metaclass__ = type |
157 | @@ -7,6 +7,10 @@ |
158 | |
159 | from BeautifulSoup import BeautifulSoup |
160 | from lazr.restful.interfaces import IJSONRequestCache |
161 | +from testscenarios import ( |
162 | + load_tests_apply_scenarios, |
163 | + WithScenarios, |
164 | + ) |
165 | import transaction |
166 | from zope.component import getUtility |
167 | from zope.publisher.interfaces import NotFound |
168 | @@ -32,6 +36,7 @@ |
169 | ) |
170 | from lp.registry.enums import BugSharingPolicy |
171 | from lp.registry.interfaces.projectgroup import IProjectGroup |
172 | +from lp.services.features.testing import FeatureFixture |
173 | from lp.services.temporaryblobstorage.interfaces import ( |
174 | ITemporaryStorageManager, |
175 | ) |
176 | @@ -787,10 +792,22 @@ |
177 | soup.find('input', attrs={'name': 'field.information_type'})) |
178 | |
179 | |
180 | -class TestFileBugSourcePackage(TestCaseWithFactory): |
181 | +class TestFileBugSourcePackage(WithScenarios, TestCaseWithFactory): |
182 | |
183 | layer = DatabaseFunctionalLayer |
184 | |
185 | + scenarios = [ |
186 | + ("bspn_picker", {"features": {}}), |
187 | + ("dsp_picker", { |
188 | + "features": {u"disclosure.dsp_picker.enabled": u"on"}, |
189 | + }), |
190 | + ] |
191 | + |
192 | + def setUp(self): |
193 | + super(TestFileBugSourcePackage, self).setUp() |
194 | + if self.features: |
195 | + self.useFixture(FeatureFixture(self.features)) |
196 | + |
197 | def test_filebug_works_on_official_package_branch(self): |
198 | # It should be possible to file a bug against a source package |
199 | # when there is an official package branch. |
200 | @@ -936,3 +953,6 @@ |
201 | login_person(user) |
202 | view = create_initialized_view(product, '+filebug', principal=user) |
203 | self._assert_cache_values(view, False) |
204 | + |
205 | + |
206 | +load_tests = load_tests_apply_scenarios |
207 | |
208 | === modified file 'lib/lp/bugs/browser/widgets/bugtask.py' |
209 | --- lib/lp/bugs/browser/widgets/bugtask.py 2016-07-23 10:28:41 +0000 |
210 | +++ lib/lp/bugs/browser/widgets/bugtask.py 2016-09-19 12:53:26 +0000 |
211 | @@ -13,6 +13,7 @@ |
212 | "BugTaskTargetWidget", |
213 | "BugWatchEditForm", |
214 | "DBItemDisplayWidget", |
215 | + "FileBugSourcePackageNameWidget", |
216 | "NewLineToSpacesWidget", |
217 | "UbuntuSourcePackageNameWidget", |
218 | ] |
219 | @@ -42,6 +43,7 @@ |
220 | InvalidValue, |
221 | ValidationError, |
222 | ) |
223 | +from zope.schema.vocabulary import getVocabularyRegistry |
224 | |
225 | from lp import _ |
226 | from lp.app.browser.tales import TeamFormatterAPI |
227 | @@ -66,7 +68,10 @@ |
228 | UnrecognizedBugTrackerURL, |
229 | ) |
230 | from lp.bugs.vocabularies import UsesBugsDistributionVocabulary |
231 | -from lp.registry.interfaces.distribution import IDistributionSet |
232 | +from lp.registry.interfaces.distribution import ( |
233 | + IDistribution, |
234 | + IDistributionSet, |
235 | + ) |
236 | from lp.services.features import getFeatureFlag |
237 | from lp.services.fields import URIField |
238 | from lp.services.webapp import canonical_url |
239 | @@ -497,7 +502,7 @@ |
240 | def getDistribution(self): |
241 | """Get the distribution used for package validation. |
242 | |
243 | - The package name has be to published in the returned distribution. |
244 | + The package name has to be published in the returned distribution. |
245 | """ |
246 | field = self.context |
247 | distribution = field.context.distribution |
248 | @@ -543,14 +548,14 @@ |
249 | BugTaskSourcePackageNameWidget): |
250 | """Package widget for +distrotask. |
251 | |
252 | - This widgets works the same as `BugTaskSourcePackageNameWidget`, |
253 | - except that it gets the distribution from the request. |
254 | + This widget works the same as `BugTaskSourcePackageNameWidget`, except |
255 | + that it gets the distribution from the request. |
256 | """ |
257 | |
258 | distribution_id = 'field.distribution' |
259 | |
260 | def getDistribution(self): |
261 | - """See `BugTaskSourcePackageNameWidget`""" |
262 | + """See `BugTaskSourcePackageNameWidget`.""" |
263 | distribution_name = self.request.form.get('field.distribution') |
264 | if distribution_name is None: |
265 | raise UnexpectedFormData( |
266 | @@ -563,6 +568,42 @@ |
267 | return distribution |
268 | |
269 | |
270 | +class FileBugSourcePackageNameWidget(BugTaskSourcePackageNameWidget): |
271 | + """Package widget for +filebug. |
272 | + |
273 | + This widget works the same as `BugTaskSourcePackageNameWidget`, except |
274 | + that it expects the field's context to be a bug target rather than a bug |
275 | + task. |
276 | + """ |
277 | + |
278 | + def getDistribution(self): |
279 | + """See `BugTaskSourcePackageNameWidget`.""" |
280 | + field = self.context |
281 | + pillar = field.context.pillar |
282 | + assert IDistribution.providedBy(pillar), ( |
283 | + "FileBugSourcePackageNameWidget should be used only for" |
284 | + " distribution bug targets.") |
285 | + return pillar |
286 | + |
287 | + def _toFieldValue(self, input): |
288 | + """See `BugTaskSourcePackageNameWidget`.""" |
289 | + source = super(FileBugSourcePackageNameWidget, self)._toFieldValue( |
290 | + input) |
291 | + if (source is not None and |
292 | + not bool(getFeatureFlag('disclosure.dsp_picker.enabled'))): |
293 | + # XXX cjwatson 2016-07-25: Convert to a value that the |
294 | + # IBug.packagename vocabulary will accept. This is a fiddly |
295 | + # hack, but it only needs to survive until we can switch to the |
296 | + # DistributionSourcePackage picker across the board. |
297 | + bspn_vocab = getVocabularyRegistry().get( |
298 | + None, "BinaryAndSourcePackageName") |
299 | + bspn = bspn_vocab.getTermByToken(source.name).value |
300 | + self.cached_values[input] = bspn |
301 | + return bspn |
302 | + else: |
303 | + return source |
304 | + |
305 | + |
306 | class UbuntuSourcePackageNameWidget(BugTaskSourcePackageNameWidget): |
307 | """A widget to select Ubuntu packages.""" |
308 | |
309 | |
310 | === modified file 'lib/lp/bugs/doc/bugtask-package-widget.txt' |
311 | --- lib/lp/bugs/doc/bugtask-package-widget.txt 2011-12-24 17:49:30 +0000 |
312 | +++ lib/lp/bugs/doc/bugtask-package-widget.txt 2016-09-19 12:53:26 +0000 |
313 | @@ -8,13 +8,17 @@ |
314 | package name, and to convert it to a source package name, we have a |
315 | custom widget. |
316 | |
317 | + >>> from lazr.restful.interface import copy_field |
318 | >>> from lp.bugs.browser.widgets.bugtask import ( |
319 | ... BugTaskSourcePackageNameWidget) |
320 | + >>> from lp.registry.interfaces.distribution import IDistributionSet |
321 | + >>> from lp.services.features import getFeatureFlag |
322 | + >>> from lp.testing import person_logged_in |
323 | |
324 | If we pass a valid source package name to it, the corresponding |
325 | -SourcePackageName will be returned by getInputValue(). In order for us |
326 | -to map the package names, we need a distribution, so we give the widget |
327 | -a distribution task to work with. |
328 | +SourcePackageName (or DistributionSourcePackage, for the new picker) will be |
329 | +returned by getInputValue(). In order for us to map the package names, we |
330 | +need a distribution, so we give the widget a distribution task to work with. |
331 | |
332 | >>> from lp.services.webapp.servers import LaunchpadTestRequest |
333 | >>> from lp.bugs.interfaces.bug import IBugSet |
334 | @@ -24,52 +28,67 @@ |
335 | >>> ubuntu_task.distribution.name |
336 | u'ubuntu' |
337 | |
338 | - >>> package_field = IBugTask['sourcepackagename'].bind(ubuntu_task) |
339 | + >>> unbound_package_field = IBugTask['sourcepackagename'] |
340 | + >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')): |
341 | + ... unbound_package_field = copy_field( |
342 | + ... unbound_package_field, |
343 | + ... vocabularyName='DistributionSourcePackage') |
344 | + ... expected_input_class = 'DistributionSourcePackage' |
345 | + ... else: |
346 | + ... expected_input_class = 'SourcePackageName' |
347 | + >>> package_field = unbound_package_field.bind(ubuntu_task) |
348 | |
349 | >>> request = LaunchpadTestRequest( |
350 | ... form={'field.sourcepackagename': 'evolution'}) |
351 | >>> widget = BugTaskSourcePackageNameWidget( |
352 | ... package_field, package_field.vocabulary, request) |
353 | - >>> widget.getInputValue() |
354 | - <SourcePackageName ...> |
355 | + >>> widget.getInputValue().__class__.__name__ == expected_input_class |
356 | + True |
357 | >>> widget.getInputValue().name |
358 | u'evolution' |
359 | |
360 | - |
361 | -If we pass in a binary package name, which can be mapped to a source |
362 | -package name, the corresponding SourcePackageName is returned. |
363 | - |
364 | +If we pass in a binary package name, which can be mapped to a source package |
365 | +name, the corresponding SourcePackageName is returned. (In the case of the |
366 | +new picker, this instead requires searching first.) |
367 | + |
368 | + >>> package_name = 'linux-2.6.12' |
369 | + >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')): |
370 | + ... package_field.vocabulary.setDistribution(ubuntu_task.distribution) |
371 | + ... results = package_field.vocabulary.searchForTerms(package_name) |
372 | + ... package_name = list(results)[0].value |
373 | >>> request = LaunchpadTestRequest( |
374 | - ... form={'field.sourcepackagename': 'linux-2.6.12'}) |
375 | + ... form={'field.sourcepackagename': package_name}) |
376 | >>> widget = BugTaskSourcePackageNameWidget( |
377 | ... package_field, package_field.vocabulary, request) |
378 | - >>> widget.getInputValue() |
379 | - <SourcePackageName ...> |
380 | + >>> widget.getInputValue().__class__.__name__ == expected_input_class |
381 | + True |
382 | >>> widget.getInputValue().name |
383 | u'linux-source-2.6.15' |
384 | |
385 | -For some distribution we don't know exactly which source packages it |
386 | -contains, so IDistribution.guessPublishedSourcePackageName will raise a |
387 | +For some distributions we don't know exactly which source packages they |
388 | +contain, so IDistribution.guessPublishedSourcePackageName will raise a |
389 | NotFoundError. |
390 | |
391 | - >>> debian_task = bug_one.bugtasks[-1] |
392 | - >>> debian_task.distribution.name |
393 | - u'debian' |
394 | - >>> debian_task.distribution.guessPublishedSourcePackageName('evolution') |
395 | + >>> gentoo = getUtility(IDistributionSet)['gentoo'] |
396 | + >>> gentoo.guessPublishedSourcePackageName('evolution') |
397 | Traceback (most recent call last): |
398 | ... |
399 | NotFoundError... |
400 | |
401 | -At that point we'll fallback to the vocabulary, so a SourcePackageName |
402 | +At that point we'll fall back to the vocabulary, so a SourcePackageName |
403 | will still be returned. |
404 | |
405 | - >>> package_field = IBugTask['sourcepackagename'].bind(debian_task) |
406 | + >>> with person_logged_in(ubuntu_task.owner): |
407 | + ... gentoo_task = bug_one.addTask(ubuntu_task.owner, gentoo) |
408 | + >>> package_field = unbound_package_field.bind(gentoo_task) |
409 | + >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')): |
410 | + ... package_field.vocabulary.setDistribution(gentoo) |
411 | >>> request = LaunchpadTestRequest( |
412 | ... form={'field.sourcepackagename': 'evolution'}) |
413 | >>> widget = BugTaskSourcePackageNameWidget( |
414 | ... package_field, package_field.vocabulary, request) |
415 | - >>> widget.getInputValue() |
416 | - <SourcePackageName ...> |
417 | + >>> widget.getInputValue().__class__.__name__ == expected_input_class |
418 | + True |
419 | >>> widget.getInputValue().name |
420 | u'evolution' |
421 | |
422 | @@ -126,3 +145,89 @@ |
423 | Traceback (most recent call last): |
424 | ... |
425 | UnexpectedFormData: ... |
426 | + |
427 | + |
428 | +FileBugSourcePackageNameWidget |
429 | +------------------------------ |
430 | + |
431 | +The +filebug page uses a widget that works much the same way as |
432 | +BugTaskSourcePackageNameWidget, except that in this case the context is a |
433 | +bug target rather than a bug task. |
434 | + |
435 | + >>> from lp.bugs.browser.widgets.bugtask import ( |
436 | + ... FileBugSourcePackageNameWidget) |
437 | + >>> from lp.bugs.interfaces.bug import IBugAddForm |
438 | + |
439 | + >>> unbound_package_field = IBugAddForm['packagename'] |
440 | + >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')): |
441 | + ... unbound_package_field = copy_field( |
442 | + ... unbound_package_field, |
443 | + ... vocabularyName='DistributionSourcePackage') |
444 | + ... expected_input_class = 'DistributionSourcePackage' |
445 | + ... else: |
446 | + ... expected_input_class = 'BinaryAndSourcePackageName' |
447 | + >>> package_field = unbound_package_field.bind(ubuntu_task.distribution) |
448 | + |
449 | + >>> request = LaunchpadTestRequest( |
450 | + ... form={'field.packagename': 'evolution'}) |
451 | + >>> widget = FileBugSourcePackageNameWidget( |
452 | + ... package_field, package_field.vocabulary, request) |
453 | + >>> widget.getInputValue().__class__.__name__ == expected_input_class |
454 | + True |
455 | + >>> widget.getInputValue().name |
456 | + u'evolution' |
457 | + |
458 | +If we pass in a binary package name, which can be mapped to a source |
459 | +package name, the corresponding source package name (albeit as a |
460 | +BinaryAndSourcePackageName) is returned. (In the case of the new picker, |
461 | +this instead requires searching first.) |
462 | + |
463 | + >>> package_name = 'linux-2.6.12' |
464 | + >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')): |
465 | + ... package_field.vocabulary.setDistribution(ubuntu_task.distribution) |
466 | + ... results = package_field.vocabulary.searchForTerms(package_name) |
467 | + ... package_name = list(results)[0].value |
468 | + >>> request = LaunchpadTestRequest( |
469 | + ... form={'field.packagename': package_name}) |
470 | + >>> widget = FileBugSourcePackageNameWidget( |
471 | + ... package_field, package_field.vocabulary, request) |
472 | + >>> widget.getInputValue().__class__.__name__ == expected_input_class |
473 | + True |
474 | + >>> widget.getInputValue().name |
475 | + u'linux-source-2.6.15' |
476 | + |
477 | +For some distributions we don't know exactly which source packages they |
478 | +contain, so IDistribution.guessPublishedSourcePackageName will raise a |
479 | +NotFoundError. |
480 | + |
481 | + >>> gentoo_task.distribution.guessPublishedSourcePackageName('evolution') |
482 | + Traceback (most recent call last): |
483 | + ... |
484 | + NotFoundError... |
485 | + |
486 | +At that point we'll fall back to the vocabulary, so a SourcePackageName |
487 | +will still be returned. |
488 | + |
489 | + >>> package_field = unbound_package_field.bind(gentoo_task.distribution) |
490 | + >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')): |
491 | + ... package_field.vocabulary.setDistribution(gentoo) |
492 | + >>> request = LaunchpadTestRequest( |
493 | + ... form={'field.packagename': 'evolution'}) |
494 | + >>> widget = FileBugSourcePackageNameWidget( |
495 | + ... package_field, package_field.vocabulary, request) |
496 | + >>> widget.getInputValue().__class__.__name__ == expected_input_class |
497 | + True |
498 | + >>> widget.getInputValue().name |
499 | + u'evolution' |
500 | + |
501 | +If we pass in a package name that doesn't exist in Launchpad, we get a |
502 | +ConversionError saying that the package name doesn't exist. |
503 | + |
504 | + >>> request = LaunchpadTestRequest( |
505 | + ... form={'field.packagename': 'no-package'}) |
506 | + >>> widget = FileBugSourcePackageNameWidget( |
507 | + ... package_field, package_field.vocabulary, request) |
508 | + >>> widget.getInputValue() |
509 | + Traceback (most recent call last): |
510 | + ... |
511 | + ConversionError... |
512 | |
513 | === modified file 'lib/lp/bugs/tests/test_doc.py' |
514 | --- lib/lp/bugs/tests/test_doc.py 2012-10-08 06:13:17 +0000 |
515 | +++ lib/lp/bugs/tests/test_doc.py 2016-09-19 12:53:26 +0000 |
516 | @@ -1,4 +1,4 @@ |
517 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
518 | +# Copyright 2009-2016 Canonical Ltd. This software is licensed under the |
519 | # GNU Affero General Public License version 3 (see the file LICENSE). |
520 | |
521 | """ |
522 | @@ -11,6 +11,7 @@ |
523 | |
524 | from lp.code.tests.test_doc import branchscannerSetUp |
525 | from lp.services.config import config |
526 | +from lp.services.features.testing import FeatureFixture |
527 | from lp.services.mail.tests.test_doc import ProcessMailLayer |
528 | from lp.soyuz.tests.test_doc import ( |
529 | lobotomize_stevea, |
530 | @@ -128,6 +129,18 @@ |
531 | login('no-priv@canonical.com') |
532 | |
533 | |
534 | +def enableDSPPickerSetUp(test): |
535 | + setUp(test) |
536 | + ff = FeatureFixture({u'disclosure.dsp_picker.enabled': u'on'}) |
537 | + ff.setUp() |
538 | + test.globs['dsp_picker_feature_fixture'] = ff |
539 | + |
540 | + |
541 | +def enableDSPPickerTearDown(test): |
542 | + test.globs['dsp_picker_feature_fixture'].cleanUp() |
543 | + tearDown(test) |
544 | + |
545 | + |
546 | special = { |
547 | 'cve-update.txt': LayeredDocFileSuite( |
548 | '../doc/cve-update.txt', |
549 | @@ -206,6 +219,18 @@ |
550 | tearDown=tearDown, |
551 | layer=LaunchpadZopelessLayer |
552 | ), |
553 | + 'bugtask-package-widget.txt': LayeredDocFileSuite( |
554 | + '../doc/bugtask-package-widget.txt', |
555 | + id_extensions=['bugtask-package-widget.txt'], |
556 | + setUp=setUp, tearDown=tearDown, |
557 | + layer=LaunchpadFunctionalLayer |
558 | + ), |
559 | + 'bugtask-package-widget.txt-dsp-picker': LayeredDocFileSuite( |
560 | + '../doc/bugtask-package-widget.txt', |
561 | + id_extensions=['bugtask-package-widget.txt-dsp-picker'], |
562 | + setUp=enableDSPPickerSetUp, tearDown=enableDSPPickerTearDown, |
563 | + layer=LaunchpadFunctionalLayer |
564 | + ), |
565 | 'bugmessage.txt': LayeredDocFileSuite( |
566 | '../doc/bugmessage.txt', |
567 | id_extensions=['bugmessage.txt'], |
568 | |
569 | === modified file 'lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py' |
570 | --- lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py 2016-09-13 12:48:02 +0000 |
571 | +++ lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py 2016-09-19 12:53:26 +0000 |
572 | @@ -86,18 +86,32 @@ |
573 | vocabulary = DistributionSourcePackageVocabulary(dsp) |
574 | self.assertIn(dsp, vocabulary) |
575 | |
576 | + def test_contains_true_with_cacheless_distribution(self): |
577 | + # The vocabulary contains DSPs that are not official, provided that |
578 | + # the distribution has no cached package names. |
579 | + dsp = self.factory.makeDistributionSourcePackage(with_db=False) |
580 | + vocabulary = DistributionSourcePackageVocabulary(dsp.distribution) |
581 | + self.assertIn(dsp, vocabulary) |
582 | + |
583 | def test_contains_false_with_distribution(self): |
584 | # The vocabulary does not contain DSPs that are not official that |
585 | # were not passed to init. |
586 | - dsp = self.factory.makeDistributionSourcePackage(with_db=False) |
587 | + distro = self.factory.makeDistribution() |
588 | + distroseries = self.factory.makeDistroSeries(distribution=distro) |
589 | + self.factory.makeDSPCache(distroseries=distroseries) |
590 | + dsp = self.factory.makeDistributionSourcePackage( |
591 | + distribution=distro, with_db=False) |
592 | vocabulary = DistributionSourcePackageVocabulary(dsp.distribution) |
593 | self.assertNotIn(dsp, vocabulary) |
594 | |
595 | def test_toTerm_raises_error(self): |
596 | # An error is raised for DSP/SPNs that are not official and are not |
597 | # in the vocabulary. |
598 | + distro = self.factory.makeDistribution() |
599 | + distroseries = self.factory.makeDistroSeries(distribution=distro) |
600 | + self.factory.makeDSPCache(distroseries=distroseries) |
601 | dsp = self.factory.makeDistributionSourcePackage( |
602 | - sourcepackagename='foo') |
603 | + sourcepackagename='foo', distribution=distro, with_db=False) |
604 | vocabulary = DistributionSourcePackageVocabulary(dsp.distribution) |
605 | self.assertRaises(LookupError, vocabulary.toTerm, dsp) |
606 | |
607 | @@ -118,6 +132,18 @@ |
608 | self.assertEqual(dsp.name, term.title) |
609 | self.assertEqual(dsp, term.value) |
610 | |
611 | + def test_toTerm_spn_with_cacheless_distribution(self): |
612 | + # An SPN with no official DSP is accepted, provided that the |
613 | + # distribution has no cached package names. |
614 | + distro = self.factory.makeDistribution() |
615 | + spn = self.factory.makeSourcePackageName() |
616 | + vocabulary = DistributionSourcePackageVocabulary(distro) |
617 | + term = vocabulary.toTerm(spn) |
618 | + self.assertEqual(spn.name, term.token) |
619 | + self.assertEqual(spn.name, term.title) |
620 | + self.assertEqual(distro, term.value.distribution) |
621 | + self.assertEqual(spn, term.value.sourcepackagename) |
622 | + |
623 | def test_toTerm_dsp(self): |
624 | # The DSP's distribution is used when a DSP is passed. |
625 | spph = self.factory.makeSourcePackagePublishingHistory() |
626 | @@ -142,10 +168,24 @@ |
627 | self.assertEqual(dsp, term.value) |
628 | self.assertEqual(['one', 'two'], term.value.binary_names) |
629 | |
630 | + def test_toTerm_dsp_with_cacheless_distribution(self): |
631 | + # A DSP that is not official is accepted, provided that the |
632 | + # distribution has no cached package names. |
633 | + dsp = self.factory.makeDistributionSourcePackage( |
634 | + sourcepackagename='foo', with_db=False) |
635 | + vocabulary = DistributionSourcePackageVocabulary(dsp.distribution) |
636 | + term = vocabulary.toTerm(dsp) |
637 | + self.assertEqual(dsp.name, term.token) |
638 | + self.assertEqual(dsp.name, term.title) |
639 | + self.assertEqual(dsp, term.value) |
640 | + |
641 | def test_getTermByToken_error(self): |
642 | # An error is raised if the token does not match a official DSP. |
643 | + distro = self.factory.makeDistribution() |
644 | + distroseries = self.factory.makeDistroSeries(distribution=distro) |
645 | + self.factory.makeDSPCache(distroseries=distroseries) |
646 | dsp = self.factory.makeDistributionSourcePackage( |
647 | - sourcepackagename='foo') |
648 | + distribution=distro, sourcepackagename='foo', with_db=False) |
649 | vocabulary = DistributionSourcePackageVocabulary(dsp.distribution) |
650 | self.assertRaises(LookupError, vocabulary.getTermByToken, dsp.name) |
651 | |
652 | @@ -158,6 +198,15 @@ |
653 | term = vocabulary.getTermByToken(dsp.name) |
654 | self.assertEqual(dsp, term.value) |
655 | |
656 | + def test_getTermByToken_token_with_cacheless_distribution(self): |
657 | + # The term is returned if it does not match an official DSP, |
658 | + # provided that the distribution has no cached package names. |
659 | + dsp = self.factory.makeDistributionSourcePackage( |
660 | + sourcepackagename='foo', with_db=False) |
661 | + vocabulary = DistributionSourcePackageVocabulary(dsp.distribution) |
662 | + term = vocabulary.getTermByToken(dsp.name) |
663 | + self.assertEqual(dsp, term.value) |
664 | + |
665 | def test_searchForTerms_without_distribution(self): |
666 | # searchForTerms asserts that the vocabulary has a distribution. |
667 | spph = self.factory.makeSourcePackagePublishingHistory() |
668 | |
669 | === modified file 'lib/lp/registry/vocabularies.py' |
670 | --- lib/lp/registry/vocabularies.py 2016-09-13 12:48:02 +0000 |
671 | +++ lib/lp/registry/vocabularies.py 2016-09-19 12:53:26 +0000 |
672 | @@ -2071,6 +2071,16 @@ |
673 | "DistributionSourcePackageVocabulary cannot be used without " |
674 | "setting a distribution.") |
675 | |
676 | + @property |
677 | + def _cache_location_clauses(self): |
678 | + return [ |
679 | + Or( |
680 | + DistributionSourcePackageCache.archiveID.is_in( |
681 | + self.distribution.all_distro_archive_ids), |
682 | + DistributionSourcePackageCache.archive == None), |
683 | + DistributionSourcePackageCache.distribution == self.distribution, |
684 | + ] |
685 | + |
686 | def toTerm(self, spn_or_dsp): |
687 | """See `IVocabulary`.""" |
688 | self._assertHasDistribution() |
689 | @@ -2089,18 +2099,34 @@ |
690 | dsp = spn_or_dsp |
691 | elif spn_or_dsp is not None: |
692 | dsp = self.distribution.getSourcePackage(spn_or_dsp) |
693 | - if dsp is not None and (dsp == self.dsp or dsp.is_official): |
694 | - if binary_names: |
695 | - # Search already did the hard work of looking up binary names. |
696 | - cache = get_property_cache(dsp) |
697 | - cache.binary_names = binary_names |
698 | - # XXX cjwatson 2016-07-22: It's a bit odd for the token to |
699 | - # return just the source package name and not the distribution |
700 | - # name as well, but at the moment this is always fed into a |
701 | - # package name box so things work much better this way. If we |
702 | - # ever do a true combined distribution/package picker, then this |
703 | - # may need to be revisited. |
704 | - return SimpleTerm(dsp, dsp.name, dsp.name) |
705 | + if dsp is not None: |
706 | + if dsp == self.dsp or dsp.is_official: |
707 | + if binary_names: |
708 | + # Search already did the hard work of looking up binary |
709 | + # names. |
710 | + cache = get_property_cache(dsp) |
711 | + cache.binary_names = binary_names |
712 | + # XXX cjwatson 2016-07-22: It's a bit odd for the token to |
713 | + # return just the source package name and not the |
714 | + # distribution name as well, but at the moment this is |
715 | + # always fed into a package name box so things work much |
716 | + # better this way. If we ever do a true combined |
717 | + # distribution/package picker, then this may need to be |
718 | + # revisited. |
719 | + return SimpleTerm(dsp, dsp.name, dsp.name) |
720 | + else: |
721 | + # Does this vocabulary have any package names at all? |
722 | + empty = IStore(DistributionSourcePackageCache).find( |
723 | + DistributionSourcePackageCache.sourcepackagenameID, |
724 | + *self._cache_location_clauses).is_empty() |
725 | + if empty: |
726 | + # If the vocabulary has no package names, then this is |
727 | + # probably a distribution not managed in Launchpad. In |
728 | + # that case we are more liberal about allowing unknown |
729 | + # package names, in order to support existing uses such |
730 | + # as noting that the same bug exists in the same package |
731 | + # in multiple distributions. |
732 | + return SimpleTerm(dsp, dsp.name, dsp.name) |
733 | raise LookupError(self.distribution, spn_or_dsp) |
734 | |
735 | def getTerm(self, spn_or_dsp): |
736 | @@ -2134,13 +2160,7 @@ |
737 | DistributionSourcePackageCache.binpkgnames.contains_string( |
738 | query), |
739 | ), |
740 | - Or( |
741 | - DistributionSourcePackageCache.archiveID.is_in( |
742 | - self.distribution.all_distro_archive_ids), |
743 | - DistributionSourcePackageCache.archive == None), |
744 | - DistributionSourcePackageCache.distribution == |
745 | - self.distribution, |
746 | - ), |
747 | + *self._cache_location_clauses), |
748 | tables=DistributionSourcePackageCache, |
749 | distinct=(DistributionSourcePackageCache.name,))) |
750 | SearchableDSPC = Table("SearchableDSPC") |