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

Proposed by Colin Watson
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 Approve
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.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/sampledata/current-dev.sql'
--- database/sampledata/current-dev.sql 2016-07-22 11:23:31 +0000
+++ database/sampledata/current-dev.sql 2016-09-19 12:53:26 +0000
@@ -3500,7 +3500,7 @@
3500INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (6, 1, 19, 'alsa-utils', '', '', '', NULL, NULL, 1);3500INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (6, 1, 19, 'alsa-utils', '', '', '', NULL, NULL, 1);
3501INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (7, 1, 20, 'cnews', '', '', '', NULL, NULL, 1);3501INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (7, 1, 20, 'cnews', '', '', '', NULL, NULL, 1);
3502INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (8, 1, 21, 'libstdc++', '', '', '', NULL, NULL, 1);3502INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (8, 1, 21, 'libstdc++', '', '', '', NULL, NULL, 1);
3503INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (9, 1, 22, 'linux-source-2.6.15', '', '', '', NULL, NULL, 1);3503INSERT 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);
3504INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (10, 1, 23, 'foobar', '', '', '', NULL, NULL, 1);3504INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (10, 1, 23, 'foobar', '', '', '', NULL, NULL, 1);
3505INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (11, 1, 27, 'commercialpackage', '', '', '', NULL, NULL, 12);3505INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (11, 1, 27, 'commercialpackage', '', '', '', NULL, NULL, 12);
35063506
35073507
=== modified file 'database/sampledata/current.sql'
--- database/sampledata/current.sql 2016-07-22 10:48:21 +0000
+++ database/sampledata/current.sql 2016-09-19 12:53:26 +0000
@@ -3434,7 +3434,7 @@
3434INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (6, 1, 19, 'alsa-utils', '', '', '', NULL, NULL, 1);3434INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (6, 1, 19, 'alsa-utils', '', '', '', NULL, NULL, 1);
3435INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (7, 1, 20, 'cnews', '', '', '', NULL, NULL, 1);3435INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (7, 1, 20, 'cnews', '', '', '', NULL, NULL, 1);
3436INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (8, 1, 21, 'libstdc++', '', '', '', NULL, NULL, 1);3436INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (8, 1, 21, 'libstdc++', '', '', '', NULL, NULL, 1);
3437INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (9, 1, 22, 'linux-source-2.6.15', '', '', '', NULL, NULL, 1);3437INSERT 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);
3438INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (10, 1, 23, 'foobar', '', '', '', NULL, NULL, 1);3438INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (10, 1, 23, 'foobar', '', '', '', NULL, NULL, 1);
3439INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (11, 1, 27, 'commercialpackage', '', '', '', NULL, NULL, 12);3439INSERT INTO distributionsourcepackagecache (id, distribution, sourcepackagename, name, binpkgnames, binpkgsummaries, binpkgdescriptions, fti, changelog, archive) VALUES (11, 1, 27, 'commercialpackage', '', '', '', NULL, NULL, 12);
34403440
34413441
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py 2016-04-29 11:11:35 +0000
+++ lib/lp/bugs/browser/bugtarget.py 2016-09-19 12:53:26 +0000
@@ -91,6 +91,7 @@
91 BugTagsWidget,91 BugTagsWidget,
92 LargeBugTagsWidget,92 LargeBugTagsWidget,
93 )93 )
94from lp.bugs.browser.widgets.bugtask import FileBugSourcePackageNameWidget
94from lp.bugs.interfaces.apportjob import IProcessApportBlobJobSource95from lp.bugs.interfaces.apportjob import IProcessApportBlobJobSource
95from lp.bugs.interfaces.bug import (96from lp.bugs.interfaces.bug import (
96 CreateBugParams,97 CreateBugParams,
@@ -131,6 +132,7 @@
131from lp.registry.interfaces.sourcepackage import ISourcePackage132from lp.registry.interfaces.sourcepackage import ISourcePackage
132from lp.registry.vocabularies import ValidPersonOrTeamVocabulary133from lp.registry.vocabularies import ValidPersonOrTeamVocabulary
133from lp.services.config import config134from lp.services.config import config
135from lp.services.features import getFeatureFlag
134from lp.services.job.interfaces.job import JobStatus136from lp.services.job.interfaces.job import JobStatus
135from lp.services.librarian.browser import ProxiedLibraryFileAlias137from lp.services.librarian.browser import ProxiedLibraryFileAlias
136from lp.services.propertycache import cachedproperty138from lp.services.propertycache import cachedproperty
@@ -225,6 +227,7 @@
225227
226 custom_widget('information_type', LaunchpadRadioWidgetWithDescription)228 custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
227 custom_widget('comment', TextAreaWidget, cssClass='comment-text')229 custom_widget('comment', TextAreaWidget, cssClass='comment-text')
230 custom_widget('packagename', FileBugSourcePackageNameWidget)
228231
229 extra_data_token = None232 extra_data_token = None
230233
@@ -419,8 +422,17 @@
419 distribution = self.context.distribution422 distribution = self.context.distribution
420423
421 try:424 try:
422 distribution.guessPublishedSourcePackageName(packagename)425 if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
423 except NotFoundError:426 dsp_vocab = self.widgets.get("packagename").vocabulary
427 dsp_vocab.setDistribution(distribution)
428 dsp_vocab.getTermByToken(packagename)
429 else:
430 # The untrusted BinaryAndSourcePackageName
431 # vocabulary was used, so it needs secondary
432 # verification.
433 distribution.guessPublishedSourcePackageName(
434 packagename)
435 except (LookupError, NotFoundError):
424 if distribution.series:436 if distribution.series:
425 # If a distribution doesn't have any series,437 # If a distribution doesn't have any series,
426 # it won't have any source packages published at438 # it won't have any source packages published at
@@ -525,24 +537,28 @@
525 information_type=information_type,537 information_type=information_type,
526 tags=data.get('tags'))538 tags=data.get('tags'))
527 if IDistribution.providedBy(context) and packagename:539 if IDistribution.providedBy(context) and packagename:
528 # We don't know if the package name we got was a source or binary540 if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
529 # package name, so let the Soyuz API figure it out for us.541 context = packagename
530 packagename = str(packagename.name)
531 try:
532 sourcepackagename = context.guessPublishedSourcePackageName(
533 packagename)
534 except NotFoundError:
535 notifications.append(
536 "The package %s is not published in %s; the "
537 "bug was targeted only to the distribution."
538 % (packagename, context.displayname))
539 params.comment += (
540 "\r\n\r\nNote: the original reporter indicated "
541 "the bug was in package %r; however, that package "
542 "was not published in %s." % (
543 packagename, context.displayname))
544 else:542 else:
545 context = context.getSourcePackage(sourcepackagename.name)543 # We don't know if the package name we got was a source or
544 # binary package name, so let the Soyuz API figure it out
545 # for us.
546 packagename = str(packagename.name)
547 try:
548 sourcepackagename = (
549 context.guessPublishedSourcePackageName(packagename))
550 except NotFoundError:
551 notifications.append(
552 "The package %s is not published in %s; the "
553 "bug was targeted only to the distribution."
554 % (packagename, context.displayname))
555 params.comment += (
556 "\r\n\r\nNote: the original reporter indicated "
557 "the bug was in package %r; however, that package "
558 "was not published in %s." % (
559 packagename, context.displayname))
560 else:
561 context = context.getSourcePackage(sourcepackagename.name)
546562
547 extra_data = self.extra_data563 extra_data = self.extra_data
548 if extra_data.extra_description:564 if extra_data.extra_description:
@@ -895,13 +911,25 @@
895 filebug_url, status=httplib.MOVED_PERMANENTLY)911 filebug_url, status=httplib.MOVED_PERMANENTLY)
896912
897913
914class IDistroBugAddForm(IBugAddForm):
915
916 packagename = copy_field(
917 IBugAddForm['packagename'], vocabularyName='DistributionSourcePackage')
918
919
898class FilebugShowSimilarBugsView(FileBugViewBase):920class FilebugShowSimilarBugsView(FileBugViewBase):
899 """A view for showing possible dupes for a bug.921 """A view for showing possible dupes for a bug.
900922
901 This view will only be used to populate asynchronously-driven parts923 This view will only be used to populate asynchronously-driven parts
902 of a page.924 of a page.
903 """925 """
904 schema = IBugAddForm926
927 @property
928 def schema(self):
929 if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
930 return IDistroBugAddForm
931 else:
932 return IBugAddForm
905933
906 # XXX: Brad Bollenbach 2006-10-04: This assignment to actions is a934 # XXX: Brad Bollenbach 2006-10-04: This assignment to actions is a
907 # hack to make the action decorator Just Work across inheritance.935 # hack to make the action decorator Just Work across inheritance.
908936
=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_filebug.py'
--- lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2016-09-19 12:53:26 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010-2012 Canonical Ltd. This software is licensed under the1# Copyright 2010-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -7,6 +7,10 @@
77
8from BeautifulSoup import BeautifulSoup8from BeautifulSoup import BeautifulSoup
9from lazr.restful.interfaces import IJSONRequestCache9from lazr.restful.interfaces import IJSONRequestCache
10from testscenarios import (
11 load_tests_apply_scenarios,
12 WithScenarios,
13 )
10import transaction14import transaction
11from zope.component import getUtility15from zope.component import getUtility
12from zope.publisher.interfaces import NotFound16from zope.publisher.interfaces import NotFound
@@ -32,6 +36,7 @@
32 )36 )
33from lp.registry.enums import BugSharingPolicy37from lp.registry.enums import BugSharingPolicy
34from lp.registry.interfaces.projectgroup import IProjectGroup38from lp.registry.interfaces.projectgroup import IProjectGroup
39from lp.services.features.testing import FeatureFixture
35from lp.services.temporaryblobstorage.interfaces import (40from lp.services.temporaryblobstorage.interfaces import (
36 ITemporaryStorageManager,41 ITemporaryStorageManager,
37 )42 )
@@ -787,10 +792,22 @@
787 soup.find('input', attrs={'name': 'field.information_type'}))792 soup.find('input', attrs={'name': 'field.information_type'}))
788793
789794
790class TestFileBugSourcePackage(TestCaseWithFactory):795class TestFileBugSourcePackage(WithScenarios, TestCaseWithFactory):
791796
792 layer = DatabaseFunctionalLayer797 layer = DatabaseFunctionalLayer
793798
799 scenarios = [
800 ("bspn_picker", {"features": {}}),
801 ("dsp_picker", {
802 "features": {u"disclosure.dsp_picker.enabled": u"on"},
803 }),
804 ]
805
806 def setUp(self):
807 super(TestFileBugSourcePackage, self).setUp()
808 if self.features:
809 self.useFixture(FeatureFixture(self.features))
810
794 def test_filebug_works_on_official_package_branch(self):811 def test_filebug_works_on_official_package_branch(self):
795 # It should be possible to file a bug against a source package812 # It should be possible to file a bug against a source package
796 # when there is an official package branch.813 # when there is an official package branch.
@@ -936,3 +953,6 @@
936 login_person(user)953 login_person(user)
937 view = create_initialized_view(product, '+filebug', principal=user)954 view = create_initialized_view(product, '+filebug', principal=user)
938 self._assert_cache_values(view, False)955 self._assert_cache_values(view, False)
956
957
958load_tests = load_tests_apply_scenarios
939959
=== modified file 'lib/lp/bugs/browser/widgets/bugtask.py'
--- lib/lp/bugs/browser/widgets/bugtask.py 2016-07-23 10:28:41 +0000
+++ lib/lp/bugs/browser/widgets/bugtask.py 2016-09-19 12:53:26 +0000
@@ -13,6 +13,7 @@
13 "BugTaskTargetWidget",13 "BugTaskTargetWidget",
14 "BugWatchEditForm",14 "BugWatchEditForm",
15 "DBItemDisplayWidget",15 "DBItemDisplayWidget",
16 "FileBugSourcePackageNameWidget",
16 "NewLineToSpacesWidget",17 "NewLineToSpacesWidget",
17 "UbuntuSourcePackageNameWidget",18 "UbuntuSourcePackageNameWidget",
18 ]19 ]
@@ -42,6 +43,7 @@
42 InvalidValue,43 InvalidValue,
43 ValidationError,44 ValidationError,
44 )45 )
46from zope.schema.vocabulary import getVocabularyRegistry
4547
46from lp import _48from lp import _
47from lp.app.browser.tales import TeamFormatterAPI49from lp.app.browser.tales import TeamFormatterAPI
@@ -66,7 +68,10 @@
66 UnrecognizedBugTrackerURL,68 UnrecognizedBugTrackerURL,
67 )69 )
68from lp.bugs.vocabularies import UsesBugsDistributionVocabulary70from lp.bugs.vocabularies import UsesBugsDistributionVocabulary
69from lp.registry.interfaces.distribution import IDistributionSet71from lp.registry.interfaces.distribution import (
72 IDistribution,
73 IDistributionSet,
74 )
70from lp.services.features import getFeatureFlag75from lp.services.features import getFeatureFlag
71from lp.services.fields import URIField76from lp.services.fields import URIField
72from lp.services.webapp import canonical_url77from lp.services.webapp import canonical_url
@@ -497,7 +502,7 @@
497 def getDistribution(self):502 def getDistribution(self):
498 """Get the distribution used for package validation.503 """Get the distribution used for package validation.
499504
500 The package name has be to published in the returned distribution.505 The package name has to be published in the returned distribution.
501 """506 """
502 field = self.context507 field = self.context
503 distribution = field.context.distribution508 distribution = field.context.distribution
@@ -543,14 +548,14 @@
543 BugTaskSourcePackageNameWidget):548 BugTaskSourcePackageNameWidget):
544 """Package widget for +distrotask.549 """Package widget for +distrotask.
545550
546 This widgets works the same as `BugTaskSourcePackageNameWidget`,551 This widget works the same as `BugTaskSourcePackageNameWidget`, except
547 except that it gets the distribution from the request.552 that it gets the distribution from the request.
548 """553 """
549554
550 distribution_id = 'field.distribution'555 distribution_id = 'field.distribution'
551556
552 def getDistribution(self):557 def getDistribution(self):
553 """See `BugTaskSourcePackageNameWidget`"""558 """See `BugTaskSourcePackageNameWidget`."""
554 distribution_name = self.request.form.get('field.distribution')559 distribution_name = self.request.form.get('field.distribution')
555 if distribution_name is None:560 if distribution_name is None:
556 raise UnexpectedFormData(561 raise UnexpectedFormData(
@@ -563,6 +568,42 @@
563 return distribution568 return distribution
564569
565570
571class FileBugSourcePackageNameWidget(BugTaskSourcePackageNameWidget):
572 """Package widget for +filebug.
573
574 This widget works the same as `BugTaskSourcePackageNameWidget`, except
575 that it expects the field's context to be a bug target rather than a bug
576 task.
577 """
578
579 def getDistribution(self):
580 """See `BugTaskSourcePackageNameWidget`."""
581 field = self.context
582 pillar = field.context.pillar
583 assert IDistribution.providedBy(pillar), (
584 "FileBugSourcePackageNameWidget should be used only for"
585 " distribution bug targets.")
586 return pillar
587
588 def _toFieldValue(self, input):
589 """See `BugTaskSourcePackageNameWidget`."""
590 source = super(FileBugSourcePackageNameWidget, self)._toFieldValue(
591 input)
592 if (source is not None and
593 not bool(getFeatureFlag('disclosure.dsp_picker.enabled'))):
594 # XXX cjwatson 2016-07-25: Convert to a value that the
595 # IBug.packagename vocabulary will accept. This is a fiddly
596 # hack, but it only needs to survive until we can switch to the
597 # DistributionSourcePackage picker across the board.
598 bspn_vocab = getVocabularyRegistry().get(
599 None, "BinaryAndSourcePackageName")
600 bspn = bspn_vocab.getTermByToken(source.name).value
601 self.cached_values[input] = bspn
602 return bspn
603 else:
604 return source
605
606
566class UbuntuSourcePackageNameWidget(BugTaskSourcePackageNameWidget):607class UbuntuSourcePackageNameWidget(BugTaskSourcePackageNameWidget):
567 """A widget to select Ubuntu packages."""608 """A widget to select Ubuntu packages."""
568609
569610
=== modified file 'lib/lp/bugs/doc/bugtask-package-widget.txt'
--- lib/lp/bugs/doc/bugtask-package-widget.txt 2011-12-24 17:49:30 +0000
+++ lib/lp/bugs/doc/bugtask-package-widget.txt 2016-09-19 12:53:26 +0000
@@ -8,13 +8,17 @@
8package name, and to convert it to a source package name, we have a8package name, and to convert it to a source package name, we have a
9custom widget.9custom widget.
1010
11 >>> from lazr.restful.interface import copy_field
11 >>> from lp.bugs.browser.widgets.bugtask import (12 >>> from lp.bugs.browser.widgets.bugtask import (
12 ... BugTaskSourcePackageNameWidget)13 ... BugTaskSourcePackageNameWidget)
14 >>> from lp.registry.interfaces.distribution import IDistributionSet
15 >>> from lp.services.features import getFeatureFlag
16 >>> from lp.testing import person_logged_in
1317
14If we pass a valid source package name to it, the corresponding18If we pass a valid source package name to it, the corresponding
15SourcePackageName will be returned by getInputValue(). In order for us19SourcePackageName (or DistributionSourcePackage, for the new picker) will be
16to map the package names, we need a distribution, so we give the widget20returned by getInputValue(). In order for us to map the package names, we
17a distribution task to work with.21need a distribution, so we give the widget a distribution task to work with.
1822
19 >>> from lp.services.webapp.servers import LaunchpadTestRequest23 >>> from lp.services.webapp.servers import LaunchpadTestRequest
20 >>> from lp.bugs.interfaces.bug import IBugSet24 >>> from lp.bugs.interfaces.bug import IBugSet
@@ -24,52 +28,67 @@
24 >>> ubuntu_task.distribution.name28 >>> ubuntu_task.distribution.name
25 u'ubuntu'29 u'ubuntu'
2630
27 >>> package_field = IBugTask['sourcepackagename'].bind(ubuntu_task)31 >>> unbound_package_field = IBugTask['sourcepackagename']
32 >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
33 ... unbound_package_field = copy_field(
34 ... unbound_package_field,
35 ... vocabularyName='DistributionSourcePackage')
36 ... expected_input_class = 'DistributionSourcePackage'
37 ... else:
38 ... expected_input_class = 'SourcePackageName'
39 >>> package_field = unbound_package_field.bind(ubuntu_task)
2840
29 >>> request = LaunchpadTestRequest(41 >>> request = LaunchpadTestRequest(
30 ... form={'field.sourcepackagename': 'evolution'})42 ... form={'field.sourcepackagename': 'evolution'})
31 >>> widget = BugTaskSourcePackageNameWidget(43 >>> widget = BugTaskSourcePackageNameWidget(
32 ... package_field, package_field.vocabulary, request)44 ... package_field, package_field.vocabulary, request)
33 >>> widget.getInputValue()45 >>> widget.getInputValue().__class__.__name__ == expected_input_class
34 <SourcePackageName ...>46 True
35 >>> widget.getInputValue().name47 >>> widget.getInputValue().name
36 u'evolution'48 u'evolution'
3749
3850If we pass in a binary package name, which can be mapped to a source package
39If we pass in a binary package name, which can be mapped to a source51name, the corresponding SourcePackageName is returned. (In the case of the
40package name, the corresponding SourcePackageName is returned.52new picker, this instead requires searching first.)
4153
54 >>> package_name = 'linux-2.6.12'
55 >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
56 ... package_field.vocabulary.setDistribution(ubuntu_task.distribution)
57 ... results = package_field.vocabulary.searchForTerms(package_name)
58 ... package_name = list(results)[0].value
42 >>> request = LaunchpadTestRequest(59 >>> request = LaunchpadTestRequest(
43 ... form={'field.sourcepackagename': 'linux-2.6.12'})60 ... form={'field.sourcepackagename': package_name})
44 >>> widget = BugTaskSourcePackageNameWidget(61 >>> widget = BugTaskSourcePackageNameWidget(
45 ... package_field, package_field.vocabulary, request)62 ... package_field, package_field.vocabulary, request)
46 >>> widget.getInputValue()63 >>> widget.getInputValue().__class__.__name__ == expected_input_class
47 <SourcePackageName ...>64 True
48 >>> widget.getInputValue().name65 >>> widget.getInputValue().name
49 u'linux-source-2.6.15'66 u'linux-source-2.6.15'
5067
51For some distribution we don't know exactly which source packages it68For some distributions we don't know exactly which source packages they
52contains, so IDistribution.guessPublishedSourcePackageName will raise a69contain, so IDistribution.guessPublishedSourcePackageName will raise a
53NotFoundError.70NotFoundError.
5471
55 >>> debian_task = bug_one.bugtasks[-1]72 >>> gentoo = getUtility(IDistributionSet)['gentoo']
56 >>> debian_task.distribution.name73 >>> gentoo.guessPublishedSourcePackageName('evolution')
57 u'debian'
58 >>> debian_task.distribution.guessPublishedSourcePackageName('evolution')
59 Traceback (most recent call last):74 Traceback (most recent call last):
60 ...75 ...
61 NotFoundError...76 NotFoundError...
6277
63At that point we'll fallback to the vocabulary, so a SourcePackageName78At that point we'll fall back to the vocabulary, so a SourcePackageName
64will still be returned.79will still be returned.
6580
66 >>> package_field = IBugTask['sourcepackagename'].bind(debian_task)81 >>> with person_logged_in(ubuntu_task.owner):
82 ... gentoo_task = bug_one.addTask(ubuntu_task.owner, gentoo)
83 >>> package_field = unbound_package_field.bind(gentoo_task)
84 >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
85 ... package_field.vocabulary.setDistribution(gentoo)
67 >>> request = LaunchpadTestRequest(86 >>> request = LaunchpadTestRequest(
68 ... form={'field.sourcepackagename': 'evolution'})87 ... form={'field.sourcepackagename': 'evolution'})
69 >>> widget = BugTaskSourcePackageNameWidget(88 >>> widget = BugTaskSourcePackageNameWidget(
70 ... package_field, package_field.vocabulary, request)89 ... package_field, package_field.vocabulary, request)
71 >>> widget.getInputValue()90 >>> widget.getInputValue().__class__.__name__ == expected_input_class
72 <SourcePackageName ...>91 True
73 >>> widget.getInputValue().name92 >>> widget.getInputValue().name
74 u'evolution'93 u'evolution'
7594
@@ -126,3 +145,89 @@
126 Traceback (most recent call last):145 Traceback (most recent call last):
127 ...146 ...
128 UnexpectedFormData: ...147 UnexpectedFormData: ...
148
149
150FileBugSourcePackageNameWidget
151------------------------------
152
153The +filebug page uses a widget that works much the same way as
154BugTaskSourcePackageNameWidget, except that in this case the context is a
155bug target rather than a bug task.
156
157 >>> from lp.bugs.browser.widgets.bugtask import (
158 ... FileBugSourcePackageNameWidget)
159 >>> from lp.bugs.interfaces.bug import IBugAddForm
160
161 >>> unbound_package_field = IBugAddForm['packagename']
162 >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
163 ... unbound_package_field = copy_field(
164 ... unbound_package_field,
165 ... vocabularyName='DistributionSourcePackage')
166 ... expected_input_class = 'DistributionSourcePackage'
167 ... else:
168 ... expected_input_class = 'BinaryAndSourcePackageName'
169 >>> package_field = unbound_package_field.bind(ubuntu_task.distribution)
170
171 >>> request = LaunchpadTestRequest(
172 ... form={'field.packagename': 'evolution'})
173 >>> widget = FileBugSourcePackageNameWidget(
174 ... package_field, package_field.vocabulary, request)
175 >>> widget.getInputValue().__class__.__name__ == expected_input_class
176 True
177 >>> widget.getInputValue().name
178 u'evolution'
179
180If we pass in a binary package name, which can be mapped to a source
181package name, the corresponding source package name (albeit as a
182BinaryAndSourcePackageName) is returned. (In the case of the new picker,
183this instead requires searching first.)
184
185 >>> package_name = 'linux-2.6.12'
186 >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
187 ... package_field.vocabulary.setDistribution(ubuntu_task.distribution)
188 ... results = package_field.vocabulary.searchForTerms(package_name)
189 ... package_name = list(results)[0].value
190 >>> request = LaunchpadTestRequest(
191 ... form={'field.packagename': package_name})
192 >>> widget = FileBugSourcePackageNameWidget(
193 ... package_field, package_field.vocabulary, request)
194 >>> widget.getInputValue().__class__.__name__ == expected_input_class
195 True
196 >>> widget.getInputValue().name
197 u'linux-source-2.6.15'
198
199For some distributions we don't know exactly which source packages they
200contain, so IDistribution.guessPublishedSourcePackageName will raise a
201NotFoundError.
202
203 >>> gentoo_task.distribution.guessPublishedSourcePackageName('evolution')
204 Traceback (most recent call last):
205 ...
206 NotFoundError...
207
208At that point we'll fall back to the vocabulary, so a SourcePackageName
209will still be returned.
210
211 >>> package_field = unbound_package_field.bind(gentoo_task.distribution)
212 >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
213 ... package_field.vocabulary.setDistribution(gentoo)
214 >>> request = LaunchpadTestRequest(
215 ... form={'field.packagename': 'evolution'})
216 >>> widget = FileBugSourcePackageNameWidget(
217 ... package_field, package_field.vocabulary, request)
218 >>> widget.getInputValue().__class__.__name__ == expected_input_class
219 True
220 >>> widget.getInputValue().name
221 u'evolution'
222
223If we pass in a package name that doesn't exist in Launchpad, we get a
224ConversionError saying that the package name doesn't exist.
225
226 >>> request = LaunchpadTestRequest(
227 ... form={'field.packagename': 'no-package'})
228 >>> widget = FileBugSourcePackageNameWidget(
229 ... package_field, package_field.vocabulary, request)
230 >>> widget.getInputValue()
231 Traceback (most recent call last):
232 ...
233 ConversionError...
129234
=== modified file 'lib/lp/bugs/tests/test_doc.py'
--- lib/lp/bugs/tests/test_doc.py 2012-10-08 06:13:17 +0000
+++ lib/lp/bugs/tests/test_doc.py 2016-09-19 12:53:26 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""4"""
@@ -11,6 +11,7 @@
1111
12from lp.code.tests.test_doc import branchscannerSetUp12from lp.code.tests.test_doc import branchscannerSetUp
13from lp.services.config import config13from lp.services.config import config
14from lp.services.features.testing import FeatureFixture
14from lp.services.mail.tests.test_doc import ProcessMailLayer15from lp.services.mail.tests.test_doc import ProcessMailLayer
15from lp.soyuz.tests.test_doc import (16from lp.soyuz.tests.test_doc import (
16 lobotomize_stevea,17 lobotomize_stevea,
@@ -128,6 +129,18 @@
128 login('no-priv@canonical.com')129 login('no-priv@canonical.com')
129130
130131
132def enableDSPPickerSetUp(test):
133 setUp(test)
134 ff = FeatureFixture({u'disclosure.dsp_picker.enabled': u'on'})
135 ff.setUp()
136 test.globs['dsp_picker_feature_fixture'] = ff
137
138
139def enableDSPPickerTearDown(test):
140 test.globs['dsp_picker_feature_fixture'].cleanUp()
141 tearDown(test)
142
143
131special = {144special = {
132 'cve-update.txt': LayeredDocFileSuite(145 'cve-update.txt': LayeredDocFileSuite(
133 '../doc/cve-update.txt',146 '../doc/cve-update.txt',
@@ -206,6 +219,18 @@
206 tearDown=tearDown,219 tearDown=tearDown,
207 layer=LaunchpadZopelessLayer220 layer=LaunchpadZopelessLayer
208 ),221 ),
222 'bugtask-package-widget.txt': LayeredDocFileSuite(
223 '../doc/bugtask-package-widget.txt',
224 id_extensions=['bugtask-package-widget.txt'],
225 setUp=setUp, tearDown=tearDown,
226 layer=LaunchpadFunctionalLayer
227 ),
228 'bugtask-package-widget.txt-dsp-picker': LayeredDocFileSuite(
229 '../doc/bugtask-package-widget.txt',
230 id_extensions=['bugtask-package-widget.txt-dsp-picker'],
231 setUp=enableDSPPickerSetUp, tearDown=enableDSPPickerTearDown,
232 layer=LaunchpadFunctionalLayer
233 ),
209 'bugmessage.txt': LayeredDocFileSuite(234 'bugmessage.txt': LayeredDocFileSuite(
210 '../doc/bugmessage.txt',235 '../doc/bugmessage.txt',
211 id_extensions=['bugmessage.txt'],236 id_extensions=['bugmessage.txt'],
212237
=== modified file 'lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py'
--- lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py 2016-09-13 12:48:02 +0000
+++ lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py 2016-09-19 12:53:26 +0000
@@ -86,18 +86,32 @@
86 vocabulary = DistributionSourcePackageVocabulary(dsp)86 vocabulary = DistributionSourcePackageVocabulary(dsp)
87 self.assertIn(dsp, vocabulary)87 self.assertIn(dsp, vocabulary)
8888
89 def test_contains_true_with_cacheless_distribution(self):
90 # The vocabulary contains DSPs that are not official, provided that
91 # the distribution has no cached package names.
92 dsp = self.factory.makeDistributionSourcePackage(with_db=False)
93 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
94 self.assertIn(dsp, vocabulary)
95
89 def test_contains_false_with_distribution(self):96 def test_contains_false_with_distribution(self):
90 # The vocabulary does not contain DSPs that are not official that97 # The vocabulary does not contain DSPs that are not official that
91 # were not passed to init.98 # were not passed to init.
92 dsp = self.factory.makeDistributionSourcePackage(with_db=False)99 distro = self.factory.makeDistribution()
100 distroseries = self.factory.makeDistroSeries(distribution=distro)
101 self.factory.makeDSPCache(distroseries=distroseries)
102 dsp = self.factory.makeDistributionSourcePackage(
103 distribution=distro, with_db=False)
93 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)104 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
94 self.assertNotIn(dsp, vocabulary)105 self.assertNotIn(dsp, vocabulary)
95106
96 def test_toTerm_raises_error(self):107 def test_toTerm_raises_error(self):
97 # An error is raised for DSP/SPNs that are not official and are not108 # An error is raised for DSP/SPNs that are not official and are not
98 # in the vocabulary.109 # in the vocabulary.
110 distro = self.factory.makeDistribution()
111 distroseries = self.factory.makeDistroSeries(distribution=distro)
112 self.factory.makeDSPCache(distroseries=distroseries)
99 dsp = self.factory.makeDistributionSourcePackage(113 dsp = self.factory.makeDistributionSourcePackage(
100 sourcepackagename='foo')114 sourcepackagename='foo', distribution=distro, with_db=False)
101 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)115 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
102 self.assertRaises(LookupError, vocabulary.toTerm, dsp)116 self.assertRaises(LookupError, vocabulary.toTerm, dsp)
103117
@@ -118,6 +132,18 @@
118 self.assertEqual(dsp.name, term.title)132 self.assertEqual(dsp.name, term.title)
119 self.assertEqual(dsp, term.value)133 self.assertEqual(dsp, term.value)
120134
135 def test_toTerm_spn_with_cacheless_distribution(self):
136 # An SPN with no official DSP is accepted, provided that the
137 # distribution has no cached package names.
138 distro = self.factory.makeDistribution()
139 spn = self.factory.makeSourcePackageName()
140 vocabulary = DistributionSourcePackageVocabulary(distro)
141 term = vocabulary.toTerm(spn)
142 self.assertEqual(spn.name, term.token)
143 self.assertEqual(spn.name, term.title)
144 self.assertEqual(distro, term.value.distribution)
145 self.assertEqual(spn, term.value.sourcepackagename)
146
121 def test_toTerm_dsp(self):147 def test_toTerm_dsp(self):
122 # The DSP's distribution is used when a DSP is passed.148 # The DSP's distribution is used when a DSP is passed.
123 spph = self.factory.makeSourcePackagePublishingHistory()149 spph = self.factory.makeSourcePackagePublishingHistory()
@@ -142,10 +168,24 @@
142 self.assertEqual(dsp, term.value)168 self.assertEqual(dsp, term.value)
143 self.assertEqual(['one', 'two'], term.value.binary_names)169 self.assertEqual(['one', 'two'], term.value.binary_names)
144170
171 def test_toTerm_dsp_with_cacheless_distribution(self):
172 # A DSP that is not official is accepted, provided that the
173 # distribution has no cached package names.
174 dsp = self.factory.makeDistributionSourcePackage(
175 sourcepackagename='foo', with_db=False)
176 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
177 term = vocabulary.toTerm(dsp)
178 self.assertEqual(dsp.name, term.token)
179 self.assertEqual(dsp.name, term.title)
180 self.assertEqual(dsp, term.value)
181
145 def test_getTermByToken_error(self):182 def test_getTermByToken_error(self):
146 # An error is raised if the token does not match a official DSP.183 # An error is raised if the token does not match a official DSP.
184 distro = self.factory.makeDistribution()
185 distroseries = self.factory.makeDistroSeries(distribution=distro)
186 self.factory.makeDSPCache(distroseries=distroseries)
147 dsp = self.factory.makeDistributionSourcePackage(187 dsp = self.factory.makeDistributionSourcePackage(
148 sourcepackagename='foo')188 distribution=distro, sourcepackagename='foo', with_db=False)
149 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)189 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
150 self.assertRaises(LookupError, vocabulary.getTermByToken, dsp.name)190 self.assertRaises(LookupError, vocabulary.getTermByToken, dsp.name)
151191
@@ -158,6 +198,15 @@
158 term = vocabulary.getTermByToken(dsp.name)198 term = vocabulary.getTermByToken(dsp.name)
159 self.assertEqual(dsp, term.value)199 self.assertEqual(dsp, term.value)
160200
201 def test_getTermByToken_token_with_cacheless_distribution(self):
202 # The term is returned if it does not match an official DSP,
203 # provided that the distribution has no cached package names.
204 dsp = self.factory.makeDistributionSourcePackage(
205 sourcepackagename='foo', with_db=False)
206 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
207 term = vocabulary.getTermByToken(dsp.name)
208 self.assertEqual(dsp, term.value)
209
161 def test_searchForTerms_without_distribution(self):210 def test_searchForTerms_without_distribution(self):
162 # searchForTerms asserts that the vocabulary has a distribution.211 # searchForTerms asserts that the vocabulary has a distribution.
163 spph = self.factory.makeSourcePackagePublishingHistory()212 spph = self.factory.makeSourcePackagePublishingHistory()
164213
=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py 2016-09-13 12:48:02 +0000
+++ lib/lp/registry/vocabularies.py 2016-09-19 12:53:26 +0000
@@ -2071,6 +2071,16 @@
2071 "DistributionSourcePackageVocabulary cannot be used without "2071 "DistributionSourcePackageVocabulary cannot be used without "
2072 "setting a distribution.")2072 "setting a distribution.")
20732073
2074 @property
2075 def _cache_location_clauses(self):
2076 return [
2077 Or(
2078 DistributionSourcePackageCache.archiveID.is_in(
2079 self.distribution.all_distro_archive_ids),
2080 DistributionSourcePackageCache.archive == None),
2081 DistributionSourcePackageCache.distribution == self.distribution,
2082 ]
2083
2074 def toTerm(self, spn_or_dsp):2084 def toTerm(self, spn_or_dsp):
2075 """See `IVocabulary`."""2085 """See `IVocabulary`."""
2076 self._assertHasDistribution()2086 self._assertHasDistribution()
@@ -2089,18 +2099,34 @@
2089 dsp = spn_or_dsp2099 dsp = spn_or_dsp
2090 elif spn_or_dsp is not None:2100 elif spn_or_dsp is not None:
2091 dsp = self.distribution.getSourcePackage(spn_or_dsp)2101 dsp = self.distribution.getSourcePackage(spn_or_dsp)
2092 if dsp is not None and (dsp == self.dsp or dsp.is_official):2102 if dsp is not None:
2093 if binary_names:2103 if dsp == self.dsp or dsp.is_official:
2094 # Search already did the hard work of looking up binary names.2104 if binary_names:
2095 cache = get_property_cache(dsp)2105 # Search already did the hard work of looking up binary
2096 cache.binary_names = binary_names2106 # names.
2097 # XXX cjwatson 2016-07-22: It's a bit odd for the token to2107 cache = get_property_cache(dsp)
2098 # return just the source package name and not the distribution2108 cache.binary_names = binary_names
2099 # name as well, but at the moment this is always fed into a2109 # XXX cjwatson 2016-07-22: It's a bit odd for the token to
2100 # package name box so things work much better this way. If we2110 # return just the source package name and not the
2101 # ever do a true combined distribution/package picker, then this2111 # distribution name as well, but at the moment this is
2102 # may need to be revisited.2112 # always fed into a package name box so things work much
2103 return SimpleTerm(dsp, dsp.name, dsp.name)2113 # better this way. If we ever do a true combined
2114 # distribution/package picker, then this may need to be
2115 # revisited.
2116 return SimpleTerm(dsp, dsp.name, dsp.name)
2117 else:
2118 # Does this vocabulary have any package names at all?
2119 empty = IStore(DistributionSourcePackageCache).find(
2120 DistributionSourcePackageCache.sourcepackagenameID,
2121 *self._cache_location_clauses).is_empty()
2122 if empty:
2123 # If the vocabulary has no package names, then this is
2124 # probably a distribution not managed in Launchpad. In
2125 # that case we are more liberal about allowing unknown
2126 # package names, in order to support existing uses such
2127 # as noting that the same bug exists in the same package
2128 # in multiple distributions.
2129 return SimpleTerm(dsp, dsp.name, dsp.name)
2104 raise LookupError(self.distribution, spn_or_dsp)2130 raise LookupError(self.distribution, spn_or_dsp)
21052131
2106 def getTerm(self, spn_or_dsp):2132 def getTerm(self, spn_or_dsp):
@@ -2134,13 +2160,7 @@
2134 DistributionSourcePackageCache.binpkgnames.contains_string(2160 DistributionSourcePackageCache.binpkgnames.contains_string(
2135 query),2161 query),
2136 ),2162 ),
2137 Or(2163 *self._cache_location_clauses),
2138 DistributionSourcePackageCache.archiveID.is_in(
2139 self.distribution.all_distro_archive_ids),
2140 DistributionSourcePackageCache.archive == None),
2141 DistributionSourcePackageCache.distribution ==
2142 self.distribution,
2143 ),
2144 tables=DistributionSourcePackageCache,2164 tables=DistributionSourcePackageCache,
2145 distinct=(DistributionSourcePackageCache.name,)))2165 distinct=(DistributionSourcePackageCache.name,)))
2146 SearchableDSPC = Table("SearchableDSPC")2166 SearchableDSPC = Table("SearchableDSPC")