Merge lp:~cjwatson/launchpad/distribution-filebug-dsp-vocab into lp:launchpad

Proposed by Colin Watson on 2016-09-07
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
Reviewer Review Type Date Requested Status
William Grant code 2016-09-07 Approve on 2016-09-19
Review via email: mp+305150@code.launchpad.net

Commit Message

Convert Distribution:+filebug and friends to use the DistributionSourcePackage picker if the appropriate feature flag is set.

Description of the Change

Convert Distribution:+filebug and friends to use the DistributionSourcePackage picker if the appropriate feature flag is set.

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 DistributionSourcePackageVocabulary.toTerm to return exact matches provided that the distribution has no rows at all in DistributionSourcePackageCache; this way it behaves as we want for at least Ubuntu, Debian, and charms, and e.g. Gentoo or openSUSE will get the more liberal treatment.

To post a comment you must log in.
18178. By Colin Watson on 2016-09-07

Update copyright.

William Grant (wgrant) :
review: Approve (code)
18179. By Colin Watson on 2016-09-19

Factor out DistributionSourcePackageVocabulary._cache_location_clauses.

18180. By Colin Watson on 2016-09-19

Merge devel.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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")