Merge lp:~cjwatson/launchpad/fix-dsp-vocab-picker into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18164
Proposed branch: lp:~cjwatson/launchpad/fix-dsp-vocab-picker
Merge into: lp:launchpad
Diff against target: 781 lines (+216/-173)
12 files modified
lib/lp/app/javascript/picker/picker_patcher.js (+4/-2)
lib/lp/app/javascript/picker/tests/test_picker_patcher.js (+35/-2)
lib/lp/app/widgets/popup.py (+14/-1)
lib/lp/app/widgets/templates/distributionsourcepackage-picker.pt (+29/-0)
lib/lp/app/widgets/templates/form-picker-macros.pt (+1/-1)
lib/lp/app/widgets/tests/test_launchpadtarget.py (+2/-12)
lib/lp/app/widgets/tests/test_popup.py (+33/-7)
lib/lp/bugs/browser/tests/test_bugalsoaffects.py (+1/-18)
lib/lp/bugs/browser/widgets/bugtask.py (+11/-2)
lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py (+45/-92)
lib/lp/registry/vocabularies.py (+29/-35)
lib/lp/services/webapp/configure.zcml (+12/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/fix-dsp-vocab-picker
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+300782@code.launchpad.net

Commit message

Customise the picker for DistributionSourcePackageVocabulary to make vocabulary requests in the context of the currently-selected distribution.

Description of the change

Customise the picker for DistributionSourcePackageVocabulary to make vocabulary requests in the context of the currently-selected distribution. Otherwise the vocabulary only works if explicitly given the <distro>/<package> search format, which isn't very user-friendly.

I've also changed the vocabulary itself to return just the package name as the term's token and title, rather than distribution/package. This is less strictly correct, but given that it's always being fed into a package name text box it works a lot better in practice (we don't want that text box to receive e.g. "ubuntu/firefox"), and since the vocabulary always has its distribution context set from somewhere else it works fine in practice.

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 'lib/lp/app/javascript/picker/picker_patcher.js'
--- lib/lp/app/javascript/picker/picker_patcher.js 2012-09-07 14:42:48 +0000
+++ lib/lp/app/javascript/picker/picker_patcher.js 2016-07-27 17:22:45 +0000
@@ -1,4 +1,4 @@
1/* Copyright 2011 Canonical Ltd. This software is licensed under the1/* Copyright 2011-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).
3 */3 */
44
@@ -620,7 +620,9 @@
620 // use the context to limit the results to the same project.620 // use the context to limit the results to the same project.
621621
622 var uri = '';622 var uri = '';
623 if (Y.Lang.isValue(config.context)){623 if (Y.Lang.isFunction(config.getContextPath)) {
624 uri += config.getContextPath() + '/';
625 } else if (Y.Lang.isValue(config.context)) {
624 uri += Y.lp.get_url_path(626 uri += Y.lp.get_url_path(
625 config.context.get('web_link')) + '/';627 config.context.get('web_link')) + '/';
626 }628 }
627629
=== modified file 'lib/lp/app/javascript/picker/tests/test_picker_patcher.js'
--- lib/lp/app/javascript/picker/tests/test_picker_patcher.js 2013-03-20 03:41:40 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker_patcher.js 2016-07-27 17:22:45 +0000
@@ -1,4 +1,4 @@
1/* Copyright 2012 Canonical Ltd. This software is licensed under the1/* Copyright 2012-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
4YUI.add('lp.app.picker.test', function (Y) {4YUI.add('lp.app.picker.test', function (Y) {
@@ -33,7 +33,7 @@
33 }33 }
3434
35 tests.suite.add(new Y.Test.Case({35 tests.suite.add(new Y.Test.Case({
36 name: 'picker_yesyno_validation',36 name: 'picker_yesno_validation',
3737
38 setUp: function() {38 setUp: function() {
39 this.vocabulary = [39 this.vocabulary = [
@@ -514,6 +514,39 @@
514514
515 tests.suite.add(new Y.Test.Case({515 tests.suite.add(new Y.Test.Case({
516516
517 name: 'picker_custom_context_path',
518
519 create_picker: function(yio, extra_config) {
520 var config = {yio: yio};
521 if (extra_config !== undefined) {
522 config = Y.merge(extra_config, config);
523 }
524 return Y.lp.app.picker.addPickerPatcher(
525 "Foo",
526 "foo/bar",
527 "test_link",
528 "picker_id",
529 config);
530 },
531
532 test_custom_context_path_honoured: function() {
533 var mock_io = new Y.lp.testing.mockio.MockIo();
534 var extra_config = {
535 "getContextPath": function() { return "/gentoo"; }
536 };
537 var picker = this.create_picker(mock_io, extra_config);
538 picker.fire('search', 'package');
539 Assert.areEqual(1, mock_io.requests.length);
540 Assert.areEqual(
541 '/gentoo/@@+huge-vocabulary?name=Foo&search_text=package&' +
542 'batch=6&start=0', mock_io.requests[0].url);
543 cleanup_widget(picker);
544 }
545
546 }));
547
548 tests.suite.add(new Y.Test.Case({
549
517 name: 'picker_error_handling',550 name: 'picker_error_handling',
518551
519 setUp: function() {552 setUp: function() {
520553
=== modified file 'lib/lp/app/widgets/popup.py'
--- lib/lp/app/widgets/popup.py 2013-04-10 08:01:20 +0000
+++ lib/lp/app/widgets/popup.py 2016-07-27 17:22:45 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 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"""Single selection widget using a popup to select one item from many."""4"""Single selection widget using a popup to select one item from many."""
@@ -275,3 +275,16 @@
275 "looking for? "275 "looking for? "
276 '<a href="%s/+affects-new-product">Register it</a>.</strong>'276 '<a href="%s/+affects-new-product">Register it</a>.</strong>'
277 % canonical_url(self.context.context))277 % canonical_url(self.context.context))
278
279
280class DistributionSourcePackagePickerWidget(VocabularyPickerWidget):
281 """Custom popup widget for choosing distribution/package combinations."""
282
283 __call__ = ViewPageTemplateFile(
284 'templates/distributionsourcepackage-picker.pt')
285
286 @property
287 def distribution_id(self):
288 return self._prefix + 'distribution'
289
290 distribution_name = ''
278291
=== added file 'lib/lp/app/widgets/templates/distributionsourcepackage-picker.pt'
--- lib/lp/app/widgets/templates/distributionsourcepackage-picker.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/app/widgets/templates/distributionsourcepackage-picker.pt 2016-07-27 17:22:45 +0000
@@ -0,0 +1,29 @@
1<tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 omit-tag="">
5
6<metal:form-picker use-macro="context/@@form-picker-macros/form-picker">
7 <script metal:fill-slot="add-picker" tal:content="structure string:
8 LPJS.use('node', 'lp.app.picker', function(Y) {
9 var config = ${view/json_config};
10 var distribution_name = '${view/distribution_name}';
11 var distribution_id = '${view/distribution_id}';
12 if (distribution_name !== '') {
13 config.getContextPath = function() {
14 return '/' + distribution_name;
15 }
16 } else if (distribution_id !== '') {
17 config.getContextPath = function() {
18 return '/' + Y.DOM.byId(distribution_id).value;
19 };
20 }
21 var show_widget_id = '${view/show_widget_id}';
22 Y.on('domready', function(e) {
23 Y.lp.app.picker.addPicker(config, show_widget_id);
24 });
25 });
26 "/>
27</metal:form-picker>
28
29</tal:root>
030
=== modified file 'lib/lp/app/widgets/templates/form-picker-macros.pt'
--- lib/lp/app/widgets/templates/form-picker-macros.pt 2012-02-01 23:47:54 +0000
+++ lib/lp/app/widgets/templates/form-picker-macros.pt 2016-07-27 17:22:45 +0000
@@ -29,7 +29,7 @@
29 </tal:search_results>29 </tal:search_results>
3030
31 <tal:chooseLink replace="structure view/chooseLink" />31 <tal:chooseLink replace="structure view/chooseLink" />
32 <script tal:content="structure string:32 <script metal:define-slot="add-picker" tal:content="structure string:
33 LPJS.use('node', 'lp.app.picker', function(Y) {33 LPJS.use('node', 'lp.app.picker', function(Y) {
34 var config = ${view/json_config};34 var config = ${view/json_config};
35 var show_widget_id = '${view/show_widget_id}';35 var show_widget_id = '${view/show_widget_id}';
3636
=== modified file 'lib/lp/app/widgets/tests/test_launchpadtarget.py'
--- lib/lp/app/widgets/tests/test_launchpadtarget.py 2016-07-11 22:35:59 +0000
+++ lib/lp/app/widgets/tests/test_launchpadtarget.py 2016-07-27 17:22:45 +0000
@@ -82,7 +82,7 @@
82 # The render template is setup.82 # The render template is setup.
83 self.assertTrue(83 self.assertTrue(
84 self.widget.template.filename.endswith('launchpad-target.pt'),84 self.widget.template.filename.endswith('launchpad-target.pt'),
85 'Template was not setup.')85 'Template was not set up.')
8686
87 def test_default_option(self):87 def test_default_option(self):
88 # This package field is the default option.88 # This package field is the default option.
@@ -198,20 +198,10 @@
198 self.widget.request = LaunchpadTestRequest(form=self.form)198 self.widget.request = LaunchpadTestRequest(form=self.form)
199 self.assertEqual(self.package, self.widget.getInputValue())199 self.assertEqual(self.package, self.widget.getInputValue())
200200
201 def test_getInputValue_package_spn_dsp_picker_feature_flag(self):
202 # The field value is the package when the package radio button
203 # is selected and the package sub field has a official dsp.
204 self.widget.request = LaunchpadTestRequest(form=self.form)
205 with FeatureFixture({u"disclosure.dsp_picker.enabled": u"on"}):
206 self.widget.setUpSubWidgets()
207 self.assertEqual(self.package, self.widget.getInputValue())
208
209 def test_getInputValue_package_dsp_dsp_picker_feature_flag(self):201 def test_getInputValue_package_dsp_dsp_picker_feature_flag(self):
210 # The field value is the package when the package radio button202 # The field value is the package when the package radio button
211 # is selected and the package sub field has valid input.203 # is selected and the package sub field has valid input.
212 form = self.form204 self.widget.request = LaunchpadTestRequest(form=self.form)
213 form['field.target.package'] = 'fnord/snarf'
214 self.widget.request = LaunchpadTestRequest(form=form)
215 with FeatureFixture({u"disclosure.dsp_picker.enabled": u"on"}):205 with FeatureFixture({u"disclosure.dsp_picker.enabled": u"on"}):
216 self.widget.setUpSubWidgets()206 self.widget.setUpSubWidgets()
217 self.assertEqual(self.package, self.widget.getInputValue())207 self.assertEqual(self.package, self.widget.getInputValue())
218208
=== modified file 'lib/lp/app/widgets/tests/test_popup.py'
--- lib/lp/app/widgets/tests/test_popup.py 2012-08-28 04:35:13 +0000
+++ lib/lp/app/widgets/tests/test_popup.py 2016-07-27 17:22:45 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010-2011 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
@@ -10,6 +10,7 @@
10from zope.schema.vocabulary import getVocabularyRegistry10from zope.schema.vocabulary import getVocabularyRegistry
1111
12from lp.app.widgets.popup import (12from lp.app.widgets.popup import (
13 DistributionSourcePackagePickerWidget,
13 PersonPickerWidget,14 PersonPickerWidget,
14 VocabularyPickerWidget,15 VocabularyPickerWidget,
15 )16 )
@@ -23,12 +24,11 @@
23 def __init__(self, name, bases=(), attrs=None, __doc__=None,24 def __init__(self, name, bases=(), attrs=None, __doc__=None,
24 __module__=None):25 __module__=None):
25 attrs = {26 attrs = {
26 "test_invalid_chars+":27 "test_invalid_chars+": Choice(vocabulary='ValidTeamOwner'),
27 Choice(vocabulary='ValidTeamOwner'),28 "test_valid.item": Choice(vocabulary='ValidTeamOwner'),
28 "test_valid.item":29 "test_filtered.item": Choice(vocabulary='DistributionOrProduct'),
29 Choice(vocabulary='ValidTeamOwner'),30 "test_target": Choice(vocabulary='DistributionSourcePackage'),
30 "test_filtered.item":31 }
31 Choice(vocabulary='DistributionOrProduct')}
32 super(TestMetaClass, self).__init__(32 super(TestMetaClass, self).__init__(
33 name, bases=bases, attrs=attrs, __doc__=__doc__,33 name, bases=bases, attrs=attrs, __doc__=__doc__,
34 __module__=__module__)34 __module__=__module__)
@@ -197,3 +197,29 @@
197 person_picker_widget.setRenderedValue(team)197 person_picker_widget.setRenderedValue(team)
198 self.assertEqual('team',198 self.assertEqual('team',
199 person_picker_widget.config['selected_value_metadata'])199 person_picker_widget.config['selected_value_metadata'])
200
201 def test_distribution_source_package_widget_distribution_id(self):
202 # The distribution source package picker refers to the correct field
203 # in distribution_id.
204 field = ITest['test_target']
205 bound_field = field.bind(self.context)
206 vocabulary = self.vocabulary_registry.get(
207 self.context, 'DistributionSourcePackage')
208 dsp_picker_widget = DistributionSourcePackagePickerWidget(
209 bound_field, vocabulary, self.request)
210 dsp_picker_widget.setPrefix('field.target')
211 self.assertEqual(
212 'field.target.distribution', dsp_picker_widget.distribution_id)
213
214 def test_distribution_source_package_widget_gets_distribution(self):
215 # The distribution source package picker gets the value of the
216 # distribution field.
217 field = ITest['test_target']
218 bound_field = field.bind(self.context)
219 vocabulary = self.vocabulary_registry.get(
220 self.context, 'DistributionSourcePackage')
221 dsp_picker_widget = DistributionSourcePackagePickerWidget(
222 bound_field, vocabulary, self.request)
223 dsp_picker_widget.setPrefix('field.target')
224 markup = dsp_picker_widget()
225 self.assertIn("distribution_id = 'field.target.distribution'", markup)
200226
=== modified file 'lib/lp/bugs/browser/tests/test_bugalsoaffects.py'
--- lib/lp/bugs/browser/tests/test_bugalsoaffects.py 2016-07-11 22:35:59 +0000
+++ lib/lp/bugs/browser/tests/test_bugalsoaffects.py 2016-07-27 17:22:45 +0000
@@ -54,24 +54,7 @@
54 browser = self.openBugPage(bug)54 browser = self.openBugPage(bug)
55 browser.getLink(url='+distrotask').click()55 browser.getLink(url='+distrotask').click()
56 browser.getControl('Distribution').value = [self.distribution.name]56 browser.getControl('Distribution').value = [self.distribution.name]
57 browser.getControl('Source Package Name').value = (57 browser.getControl('Source Package Name').value = dsp.name
58 dsp.sourcepackagename.name)
59 browser.getControl('Continue').click()
60 self.assertEqual([], get_feedback_messages(browser.contents))
61
62 def test_bug_alsoaffects_dsp_exists_dsp_picker_feature_flag(self):
63 # If the distribution source package is official, there is no error.
64 bug = self.factory.makeBug()
65 distroseries = self.factory.makeDistroSeries(
66 distribution=self.distribution)
67 dsp = self.factory.makeDSPCache(
68 distroseries=distroseries, sourcepackagename='snarf')
69 with FeatureFixture({u"disclosure.dsp_picker.enabled": u"on"}):
70 browser = self.openBugPage(bug)
71 browser.getLink(url='+distrotask').click()
72 browser.getControl('Distribution').value = [self.distribution.name]
73 browser.getControl('Source Package Name').value = (
74 '%s/%s' % (self.distribution.name, dsp.name))
75 browser.getControl('Continue').click()58 browser.getControl('Continue').click()
76 self.assertEqual([], get_feedback_messages(browser.contents))59 self.assertEqual([], get_feedback_messages(browser.contents))
7760
7861
=== modified file 'lib/lp/bugs/browser/widgets/bugtask.py'
--- lib/lp/bugs/browser/widgets/bugtask.py 2016-06-10 22:06:13 +0000
+++ lib/lp/bugs/browser/widgets/bugtask.py 2016-07-27 17:22:45 +0000
@@ -53,8 +53,8 @@
53from lp.app.widgets.helpers import get_widget_template53from lp.app.widgets.helpers import get_widget_template
54from lp.app.widgets.launchpadtarget import LaunchpadTargetWidget54from lp.app.widgets.launchpadtarget import LaunchpadTargetWidget
55from lp.app.widgets.popup import (55from lp.app.widgets.popup import (
56 DistributionSourcePackagePickerWidget,
56 PersonPickerWidget,57 PersonPickerWidget,
57 VocabularyPickerWidget,
58 )58 )
59from lp.app.widgets.textwidgets import (59from lp.app.widgets.textwidgets import (
60 StrippedTextWidget,60 StrippedTextWidget,
@@ -478,12 +478,17 @@
478 return vocabulary478 return vocabulary
479479
480480
481class BugTaskSourcePackageNameWidget(VocabularyPickerWidget):481class BugTaskSourcePackageNameWidget(DistributionSourcePackagePickerWidget):
482 """A widget for associating a bugtask with a SourcePackageName.482 """A widget for associating a bugtask with a SourcePackageName.
483483
484 It accepts both binary and source package names.484 It accepts both binary and source package names.
485 """485 """
486486
487 # Pages that use this widget don't display the distribution, but this
488 # can only be used by bugtasks on the distribution in question so the
489 # vocabulary will be able to work it out for itself.
490 distribution_id = ''
491
487 def __init__(self, field, vocabulary, request):492 def __init__(self, field, vocabulary, request):
488 super(BugTaskSourcePackageNameWidget, self).__init__(493 super(BugTaskSourcePackageNameWidget, self).__init__(
489 field, vocabulary, request)494 field, vocabulary, request)
@@ -542,6 +547,8 @@
542 except that it gets the distribution from the request.547 except that it gets the distribution from the request.
543 """548 """
544549
550 distribution_id = 'field.distribution'
551
545 def getDistribution(self):552 def getDistribution(self):
546 """See `BugTaskSourcePackageNameWidget`"""553 """See `BugTaskSourcePackageNameWidget`"""
547 distribution_name = self.request.form.get('field.distribution')554 distribution_name = self.request.form.get('field.distribution')
@@ -559,6 +566,8 @@
559class UbuntuSourcePackageNameWidget(BugTaskSourcePackageNameWidget):566class UbuntuSourcePackageNameWidget(BugTaskSourcePackageNameWidget):
560 """A widget to select Ubuntu packages."""567 """A widget to select Ubuntu packages."""
561568
569 distribution_name = 'ubuntu'
570
562 def getDistribution(self):571 def getDistribution(self):
563 """See `BugTaskSourcePackageNameWidget`"""572 """See `BugTaskSourcePackageNameWidget`"""
564 return getUtility(ILaunchpadCelebrities).ubuntu573 return getUtility(ILaunchpadCelebrities).ubuntu
565574
=== modified file 'lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py'
--- lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py 2016-07-11 22:35:59 +0000
+++ lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py 2016-07-27 17:22:45 +0000
@@ -69,52 +69,24 @@
69 vocabulary.setDistribution(new_distro)69 vocabulary.setDistribution(new_distro)
70 self.assertEqual(new_distro, vocabulary.distribution)70 self.assertEqual(new_distro, vocabulary.distribution)
7171
72 def test_parseToken_distro_and_package(self):72 def test_contains_true_with_distribution(self):
73 # parseToken returns a tuple of distribution and package name when
74 # the text contains both.
75 new_distro = self.factory.makeDistribution(name='fnord')
76 vocabulary = DistributionSourcePackageVocabulary(None)
77 distribution, package_name = vocabulary.parseToken('fnord/pting')
78 self.assertEqual(new_distro, distribution)
79 self.assertEqual('pting', package_name)
80
81 def test_parseToken_default_distro_and_package(self):
82 # parseToken returns a tuple of the default distribution and package
83 # name when the text is just a package name.
84 default_distro = self.factory.makeDistribution(name='fnord')
85 vocabulary = DistributionSourcePackageVocabulary(default_distro)
86 distribution, package_name = vocabulary.parseToken('pting')
87 self.assertEqual(default_distro, distribution)
88 self.assertEqual('pting', package_name)
89
90 def test_parseToken_bad_distro_and_package(self):
91 # parseToken returns a tuple of the default distribution and package
92 # name when the distro in the text cannot be matched to a real
93 # distro.
94 default_distro = self.factory.makeDistribution(name='fnord')
95 vocabulary = DistributionSourcePackageVocabulary(default_distro)
96 distribution, package_name = vocabulary.parseToken('misspelled/pting')
97 self.assertEqual(default_distro, distribution)
98 self.assertEqual('pting', package_name)
99
100 def test_contains_true_without_init(self):
101 # The vocabulary contains official DSPs.73 # The vocabulary contains official DSPs.
102 dsp = self.factory.makeDistributionSourcePackage(with_db=True)74 dsp = self.factory.makeDistributionSourcePackage(with_db=True)
103 vocabulary = DistributionSourcePackageVocabulary(None)75 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
104 self.assertIn(dsp, vocabulary)76 self.assertIn(dsp, vocabulary)
10577
106 def test_contains_true_with_init(self):78 def test_contains_true_with_dsp(self):
107 # The vocabulary contains the DSP passed to init when it is not79 # The vocabulary contains the DSP passed to init when it is not
108 # official.80 # official.
109 dsp = self.factory.makeDistributionSourcePackage(with_db=False)81 dsp = self.factory.makeDistributionSourcePackage(with_db=False)
110 vocabulary = DistributionSourcePackageVocabulary(dsp)82 vocabulary = DistributionSourcePackageVocabulary(dsp)
111 self.assertIn(dsp, vocabulary)83 self.assertIn(dsp, vocabulary)
11284
113 def test_contains_false_without_init(self):85 def test_contains_false_with_distribution(self):
114 # The vocabulary does not contain DSPs that are not official that86 # The vocabulary does not contain DSPs that are not official that
115 # were not passed to init.87 # were not passed to init.
116 dsp = self.factory.makeDistributionSourcePackage(with_db=False)88 dsp = self.factory.makeDistributionSourcePackage(with_db=False)
117 vocabulary = DistributionSourcePackageVocabulary(None)89 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
118 self.assertNotIn(dsp, vocabulary)90 self.assertNotIn(dsp, vocabulary)
11991
120 def test_toTerm_raises_error(self):92 def test_toTerm_raises_error(self):
@@ -123,11 +95,12 @@
123 dsp = self.factory.makeDistributionSourcePackage(95 dsp = self.factory.makeDistributionSourcePackage(
124 sourcepackagename='foo')96 sourcepackagename='foo')
125 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)97 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
126 self.assertRaises(LookupError, vocabulary.toTerm, dsp.name)98 self.assertRaises(LookupError, vocabulary.toTerm, dsp)
12799
128 def test_toTerm_none_raises_error(self):100 def test_toTerm_none_raises_error(self):
129 # An error is raised for an SPN that does not exist.101 # An error is raised for an SPN that does not exist.
130 vocabulary = DistributionSourcePackageVocabulary(None)102 vocabulary = DistributionSourcePackageVocabulary(
103 self.factory.makeDistribution())
131 self.assertRaises(LookupError, vocabulary.toTerm, 'nonexistent')104 self.assertRaises(LookupError, vocabulary.toTerm, 'nonexistent')
132105
133 def test_toTerm_spn_and_default_distribution(self):106 def test_toTerm_spn_and_default_distribution(self):
@@ -137,21 +110,8 @@
137 spph.sourcepackagerelease.sourcepackagename)110 spph.sourcepackagerelease.sourcepackagename)
138 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)111 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
139 term = vocabulary.toTerm(dsp.sourcepackagename)112 term = vocabulary.toTerm(dsp.sourcepackagename)
140 expected_token = '%s/%s' % (dsp.distribution.name, dsp.name)113 self.assertEqual(dsp.name, term.token)
141 self.assertEqual(expected_token, term.token)114 self.assertEqual(dsp.name, term.title)
142 self.assertEqual(expected_token, term.title)
143 self.assertEqual(dsp, term.value)
144
145 def test_toTerm_spn_and_distribution(self):
146 # The distribution is used with the spn if it is passed.
147 spph = self.factory.makeSourcePackagePublishingHistory()
148 dsp = spph.distroseries.distribution.getSourcePackage(
149 spph.sourcepackagerelease.sourcepackagename)
150 vocabulary = DistributionSourcePackageVocabulary(None)
151 term = vocabulary.toTerm(dsp.sourcepackagename, dsp.distribution)
152 expected_token = '%s/%s' % (dsp.distribution.name, dsp.name)
153 self.assertEqual(expected_token, term.token)
154 self.assertEqual(expected_token, term.title)
155 self.assertEqual(dsp, term.value)115 self.assertEqual(dsp, term.value)
156116
157 def test_toTerm_dsp(self):117 def test_toTerm_dsp(self):
@@ -161,9 +121,8 @@
161 spph.sourcepackagerelease.sourcepackagename)121 spph.sourcepackagerelease.sourcepackagename)
162 vocabulary = DistributionSourcePackageVocabulary(dsp)122 vocabulary = DistributionSourcePackageVocabulary(dsp)
163 term = vocabulary.toTerm(dsp)123 term = vocabulary.toTerm(dsp)
164 expected_token = '%s/%s' % (dsp.distribution.name, dsp.name)124 self.assertEqual(dsp.name, term.token)
165 self.assertEqual(expected_token, term.token)125 self.assertEqual(dsp.name, term.title)
166 self.assertEqual(expected_token, term.title)
167 self.assertEqual(dsp, term.value)126 self.assertEqual(dsp, term.value)
168127
169 def test_toTerm_dsp_and_binary_names(self):128 def test_toTerm_dsp_and_binary_names(self):
@@ -174,9 +133,8 @@
174 spph.sourcepackagerelease.sourcepackagename)133 spph.sourcepackagerelease.sourcepackagename)
175 vocabulary = DistributionSourcePackageVocabulary(dsp)134 vocabulary = DistributionSourcePackageVocabulary(dsp)
176 term = vocabulary.toTerm((dsp, 'one two'))135 term = vocabulary.toTerm((dsp, 'one two'))
177 expected_token = '%s/%s' % (dsp.distribution.name, dsp.name)136 self.assertEqual(dsp.name, term.token)
178 self.assertEqual(expected_token, term.token)137 self.assertEqual(dsp.name, term.title)
179 self.assertEqual(expected_token, term.title)
180 self.assertEqual(dsp, term.value)138 self.assertEqual(dsp, term.value)
181 self.assertEqual(['one', 'two'], term.value.binary_names)139 self.assertEqual(['one', 'two'], term.value.binary_names)
182140
@@ -185,8 +143,7 @@
185 dsp = self.factory.makeDistributionSourcePackage(143 dsp = self.factory.makeDistributionSourcePackage(
186 sourcepackagename='foo')144 sourcepackagename='foo')
187 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)145 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
188 token = '%s/%s' % (dsp.distribution.name, dsp.name)146 self.assertRaises(LookupError, vocabulary.getTermByToken, dsp.name)
189 self.assertRaises(LookupError, vocabulary.getTermByToken, token)
190147
191 def test_getTermByToken_token(self):148 def test_getTermByToken_token(self):
192 # The term is returned if it matches an official DSP.149 # The term is returned if it matches an official DSP.
@@ -194,20 +151,16 @@
194 dsp = spph.distroseries.distribution.getSourcePackage(151 dsp = spph.distroseries.distribution.getSourcePackage(
195 spph.sourcepackagerelease.sourcepackagename)152 spph.sourcepackagerelease.sourcepackagename)
196 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)153 vocabulary = DistributionSourcePackageVocabulary(dsp.distribution)
197 token = '%s/%s' % (dsp.distribution.name, dsp.name)154 term = vocabulary.getTermByToken(dsp.name)
198 term = vocabulary.getTermByToken(token)
199 self.assertEqual(dsp, term.value)155 self.assertEqual(dsp, term.value)
200156
201 def test_searchForTerms_without_distribution(self):157 def test_searchForTerms_without_distribution(self):
202 # An empty result set is returned if the vocabulary has no158 # searchForTerms asserts that the vocabulary has a distribution.
203 # distribution and the search does not provide distribution
204 # information.
205 spph = self.factory.makeSourcePackagePublishingHistory()159 spph = self.factory.makeSourcePackagePublishingHistory()
206 dsp = spph.distroseries.distribution.getSourcePackage(160 dsp = spph.distroseries.distribution.getSourcePackage(
207 spph.sourcepackagerelease.sourcepackagename)161 spph.sourcepackagerelease.sourcepackagename)
208 vocabulary = DistributionSourcePackageVocabulary(dsp.name)162 vocabulary = DistributionSourcePackageVocabulary(dsp.name)
209 results = vocabulary.searchForTerms(dsp.name)163 self.assertRaises(AssertionError, vocabulary.searchForTerms, dsp.name)
210 self.assertEqual(0, results.count())
211164
212 def test_searchForTerms_None(self):165 def test_searchForTerms_None(self):
213 # Searching for nothing gets you that.166 # Searching for nothing gets you that.
@@ -222,11 +175,11 @@
222 distroseries = self.factory.makeDistroSeries(distribution=distro)175 distroseries = self.factory.makeDistroSeries(distribution=distro)
223 self.factory.makeDSPCache(176 self.factory.makeDSPCache(
224 distroseries=distroseries, sourcepackagename='snarf')177 distroseries=distroseries, sourcepackagename='snarf')
225 vocabulary = DistributionSourcePackageVocabulary(None)178 vocabulary = DistributionSourcePackageVocabulary(distro)
226 results = vocabulary.searchForTerms(query='fnord/snarf')179 results = vocabulary.searchForTerms(query='snarf')
227 terms = list(results)180 terms = list(results)
228 self.assertEqual(1, len(terms))181 self.assertEqual(1, len(terms))
229 self.assertEqual('fnord/snarf', terms[0].token)182 self.assertEqual('snarf', terms[0].token)
230183
231 def test_searchForTerms_similar_official_source_name(self):184 def test_searchForTerms_similar_official_source_name(self):
232 # Partial source name matches are found.185 # Partial source name matches are found.
@@ -234,11 +187,11 @@
234 distroseries = self.factory.makeDistroSeries(distribution=distro)187 distroseries = self.factory.makeDistroSeries(distribution=distro)
235 self.factory.makeDSPCache(188 self.factory.makeDSPCache(
236 distroseries=distroseries, sourcepackagename='pting-snarf-ack')189 distroseries=distroseries, sourcepackagename='pting-snarf-ack')
237 vocabulary = DistributionSourcePackageVocabulary(None)190 vocabulary = DistributionSourcePackageVocabulary(distro)
238 results = vocabulary.searchForTerms(query='fnord/snarf')191 results = vocabulary.searchForTerms(query='snarf')
239 terms = list(results)192 terms = list(results)
240 self.assertEqual(1, len(terms))193 self.assertEqual(1, len(terms))
241 self.assertEqual('fnord/pting-snarf-ack', terms[0].token)194 self.assertEqual('pting-snarf-ack', terms[0].token)
242195
243 def test_searchForTerms_exact_binary_name(self):196 def test_searchForTerms_exact_binary_name(self):
244 # Exact binary name matches are found.197 # Exact binary name matches are found.
@@ -247,11 +200,11 @@
247 self.factory.makeDSPCache(200 self.factory.makeDSPCache(
248 distroseries=distroseries, sourcepackagename='snarf',201 distroseries=distroseries, sourcepackagename='snarf',
249 binary_names='pting-dev pting ack')202 binary_names='pting-dev pting ack')
250 vocabulary = DistributionSourcePackageVocabulary(None)203 vocabulary = DistributionSourcePackageVocabulary(distro)
251 results = vocabulary.searchForTerms(query='fnord/pting')204 results = vocabulary.searchForTerms(query='pting')
252 terms = list(results)205 terms = list(results)
253 self.assertEqual(1, len(terms))206 self.assertEqual(1, len(terms))
254 self.assertEqual('fnord/snarf', terms[0].token)207 self.assertEqual('snarf', terms[0].token)
255208
256 def test_searchForTerms_similar_binary_name(self):209 def test_searchForTerms_similar_binary_name(self):
257 # Partial binary name matches are found.210 # Partial binary name matches are found.
@@ -260,11 +213,11 @@
260 self.factory.makeDSPCache(213 self.factory.makeDSPCache(
261 distroseries=distroseries, sourcepackagename='snarf',214 distroseries=distroseries, sourcepackagename='snarf',
262 binary_names='thrpp pting-dev ack')215 binary_names='thrpp pting-dev ack')
263 vocabulary = DistributionSourcePackageVocabulary(None)216 vocabulary = DistributionSourcePackageVocabulary(distro)
264 results = vocabulary.searchForTerms(query='fnord/pting')217 results = vocabulary.searchForTerms(query='pting')
265 terms = list(results)218 terms = list(results)
266 self.assertEqual(1, len(terms))219 self.assertEqual(1, len(terms))
267 self.assertEqual('fnord/snarf', terms[0].token)220 self.assertEqual('snarf', terms[0].token)
268221
269 def test_searchForTerms_exact_unofficial_source_name(self):222 def test_searchForTerms_exact_unofficial_source_name(self):
270 # Unofficial source packages are not found by search.223 # Unofficial source packages are not found by search.
@@ -273,8 +226,8 @@
273 self.factory.makeDSPCache(226 self.factory.makeDSPCache(
274 distroseries=distroseries, sourcepackagename='snarf',227 distroseries=distroseries, sourcepackagename='snarf',
275 official=False)228 official=False)
276 vocabulary = DistributionSourcePackageVocabulary(None)229 vocabulary = DistributionSourcePackageVocabulary(distro)
277 results = vocabulary.searchForTerms(query='fnord/snarf')230 results = vocabulary.searchForTerms(query='snarf')
278 terms = list(results)231 terms = list(results)
279 self.assertEqual(0, len(terms))232 self.assertEqual(0, len(terms))
280233
@@ -285,8 +238,8 @@
285 self.factory.makeDSPCache(238 self.factory.makeDSPCache(
286 distroseries=distroseries, sourcepackagename='snarf',239 distroseries=distroseries, sourcepackagename='snarf',
287 official=False, binary_names='thrpp pting ack')240 official=False, binary_names='thrpp pting ack')
288 vocabulary = DistributionSourcePackageVocabulary(None)241 vocabulary = DistributionSourcePackageVocabulary(distro)
289 results = vocabulary.searchForTerms(query='fnord/pting')242 results = vocabulary.searchForTerms(query='pting')
290 terms = list(results)243 terms = list(results)
291 self.assertEqual(0, len(terms))244 self.assertEqual(0, len(terms))
292245
@@ -304,14 +257,14 @@
304 self.factory.makeDSPCache(257 self.factory.makeDSPCache(
305 distroseries=distroseries, sourcepackagename='pting-client',258 distroseries=distroseries, sourcepackagename='pting-client',
306 binary_names='snarf-common')259 binary_names='snarf-common')
307 vocabulary = DistributionSourcePackageVocabulary(None)260 vocabulary = DistributionSourcePackageVocabulary(distro)
308 results = vocabulary.searchForTerms(query='fnord/snarf')261 results = vocabulary.searchForTerms(query='snarf')
309 terms = list(results)262 terms = list(results)
310 self.assertEqual(4, len(terms))263 self.assertEqual(4, len(terms))
311 self.assertEqual('fnord/snarf', terms[0].token)264 self.assertEqual('snarf', terms[0].token)
312 self.assertEqual('fnord/pting-devel', terms[1].token)265 self.assertEqual('pting-devel', terms[1].token)
313 self.assertEqual('fnord/snarf-server', terms[2].token)266 self.assertEqual('snarf-server', terms[2].token)
314 self.assertEqual('fnord/pting-client', terms[3].token)267 self.assertEqual('pting-client', terms[3].token)
315268
316 def test_searchForTerms_partner_archive(self):269 def test_searchForTerms_partner_archive(self):
317 # Packages in partner archives are searched.270 # Packages in partner archives are searched.
@@ -321,11 +274,11 @@
321 distroseries=distroseries, sourcepackagename='snarf',274 distroseries=distroseries, sourcepackagename='snarf',
322 archive=self.factory.makeArchive(275 archive=self.factory.makeArchive(
323 distribution=distro, purpose=ArchivePurpose.PARTNER))276 distribution=distro, purpose=ArchivePurpose.PARTNER))
324 vocabulary = DistributionSourcePackageVocabulary(None)277 vocabulary = DistributionSourcePackageVocabulary(distro)
325 results = vocabulary.searchForTerms(query='fnord/snarf')278 results = vocabulary.searchForTerms(query='snarf')
326 terms = list(results)279 terms = list(results)
327 self.assertEqual(1, len(terms))280 self.assertEqual(1, len(terms))
328 self.assertEqual('fnord/snarf', terms[0].token)281 self.assertEqual('snarf', terms[0].token)
329282
330 def test_searchForTerms_ppa_archive(self):283 def test_searchForTerms_ppa_archive(self):
331 # Packages in PPAs are ignored.284 # Packages in PPAs are ignored.
@@ -336,6 +289,6 @@
336 official=False,289 official=False,
337 archive=self.factory.makeArchive(290 archive=self.factory.makeArchive(
338 distribution=distro, purpose=ArchivePurpose.PPA))291 distribution=distro, purpose=ArchivePurpose.PPA))
339 vocabulary = DistributionSourcePackageVocabulary(None)292 vocabulary = DistributionSourcePackageVocabulary(distro)
340 results = vocabulary.searchForTerms(query='fnord/snarf')293 results = vocabulary.searchForTerms(query='snarf')
341 self.assertEqual(0, results.count())294 self.assertEqual(0, results.count())
342295
=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py 2016-07-11 21:46:03 +0000
+++ lib/lp/registry/vocabularies.py 2016-07-27 17:22:45 +0000
@@ -111,10 +111,7 @@
111 PersonVisibility,111 PersonVisibility,
112 )112 )
113from lp.registry.interfaces.accesspolicy import IAccessPolicySource113from lp.registry.interfaces.accesspolicy import IAccessPolicySource
114from lp.registry.interfaces.distribution import (114from lp.registry.interfaces.distribution import IDistribution
115 IDistribution,
116 IDistributionSet,
117 )
118from lp.registry.interfaces.distributionsourcepackage import (115from lp.registry.interfaces.distributionsourcepackage import (
119 IDistributionSourcePackage,116 IDistributionSourcePackage,
120 )117 )
@@ -2028,7 +2025,7 @@
2028class DistributionSourcePackageVocabulary(FilteredVocabularyBase):2025class DistributionSourcePackageVocabulary(FilteredVocabularyBase):
20292026
2030 displayname = 'Select a package'2027 displayname = 'Select a package'
2031 step_title = 'Search by name or distro/name'2028 step_title = 'Search by name'
20322029
2033 def __init__(self, context):2030 def __init__(self, context):
2034 self.context = context2031 self.context = context
@@ -2068,19 +2065,15 @@
2068 """Set the distribution after the vocabulary was instantiated."""2065 """Set the distribution after the vocabulary was instantiated."""
2069 self.distribution = distribution2066 self.distribution = distribution
20702067
2071 def parseToken(self, text):2068 def _assertHasDistribution(self):
2072 """Return the distribution and package name from the parsed token."""2069 if self.distribution is None:
2073 # Match the toTerm() format, but also use it to select a distribution.2070 raise AssertionError(
2074 distribution = None2071 "DistributionSourcePackageVocabulary cannot be used without "
2075 if '/' in text:2072 "setting a distribution.")
2076 distro_name, text = text.split('/', 1)
2077 distribution = getUtility(IDistributionSet).getByName(distro_name)
2078 if distribution is None:
2079 distribution = self.distribution
2080 return distribution, text
20812073
2082 def toTerm(self, spn_or_dsp, distribution=None):2074 def toTerm(self, spn_or_dsp):
2083 """See `IVocabulary`."""2075 """See `IVocabulary`."""
2076 self._assertHasDistribution()
2084 dsp = None2077 dsp = None
2085 binary_names = None2078 binary_names = None
2086 if isinstance(spn_or_dsp, tuple):2079 if isinstance(spn_or_dsp, tuple):
@@ -2088,21 +2081,27 @@
2088 spn_or_dsp, binary_names = spn_or_dsp2081 spn_or_dsp, binary_names = spn_or_dsp
2089 if binary_names is not None:2082 if binary_names is not None:
2090 binary_names = binary_names.split()2083 binary_names = binary_names.split()
2084 # XXX cjwatson 2016-07-27: Eventually this should just take a DSP
2085 # and drop the complication of also accepting SPNs; but, for now,
2086 # accepting an SPN reduces the amount of feature-flag checks
2087 # required by users of this vocabulary.
2091 if IDistributionSourcePackage.providedBy(spn_or_dsp):2088 if IDistributionSourcePackage.providedBy(spn_or_dsp):
2092 dsp = spn_or_dsp2089 dsp = spn_or_dsp
2093 distribution = spn_or_dsp.distribution2090 elif spn_or_dsp is not None:
2094 else:2091 dsp = self.distribution.getSourcePackage(spn_or_dsp)
2095 distribution = distribution or self.distribution
2096 if distribution is not None and spn_or_dsp is not None:
2097 dsp = distribution.getSourcePackage(spn_or_dsp)
2098 if dsp is not None and (dsp == self.dsp or dsp.is_official):2092 if dsp is not None and (dsp == self.dsp or dsp.is_official):
2099 if binary_names:2093 if binary_names:
2100 # Search already did the hard work of looking up binary names.2094 # Search already did the hard work of looking up binary names.
2101 cache = get_property_cache(dsp)2095 cache = get_property_cache(dsp)
2102 cache.binary_names = binary_names2096 cache.binary_names = binary_names
2103 token = '%s/%s' % (dsp.distribution.name, dsp.name)2097 # XXX cjwatson 2016-07-22: It's a bit odd for the token to
2104 return SimpleTerm(dsp, token, token)2098 # return just the source package name and not the distribution
2105 raise LookupError(distribution, spn_or_dsp)2099 # name as well, but at the moment this is always fed into a
2100 # package name box so things work much better this way. If we
2101 # ever do a true combined distribution/package picker, then this
2102 # may need to be revisited.
2103 return SimpleTerm(dsp, dsp.name, dsp.name)
2104 raise LookupError(self.distribution, spn_or_dsp)
21062105
2107 def getTerm(self, spn_or_dsp):2106 def getTerm(self, spn_or_dsp):
2108 """See `IBaseVocabulary`."""2107 """See `IBaseVocabulary`."""
@@ -2110,19 +2109,13 @@
21102109
2111 def getTermByToken(self, token):2110 def getTermByToken(self, token):
2112 """See `IVocabularyTokenized`."""2111 """See `IVocabularyTokenized`."""
2113 distribution, package_name = self.parseToken(token)2112 return self.toTerm(token)
2114 return self.toTerm(package_name, distribution)
21152113
2116 def searchForTerms(self, query=None, vocab_filter=None):2114 def searchForTerms(self, query=None, vocab_filter=None):
2117 """See `IHugeVocabulary`."""2115 """See `IHugeVocabulary`."""
2116 self._assertHasDistribution()
2118 if not query:2117 if not query:
2119 return EmptyResultSet()2118 return EmptyResultSet()
2120 distribution, query = self.parseToken(query)
2121 if distribution is None:
2122 # This could failover to ubuntu, but that is non-obvious. The
2123 # Python widget must set the default distribution and the JS
2124 # widget must encourage the <distro>/<package> search format.
2125 return EmptyResultSet()
21262119
2127 query = unicode(query)2120 query = unicode(query)
2128 query_re = re.escape(query)2121 query_re = re.escape(query)
@@ -2143,9 +2136,10 @@
2143 ),2136 ),
2144 Or(2137 Or(
2145 DistributionSourcePackageCache.archiveID.is_in(2138 DistributionSourcePackageCache.archiveID.is_in(
2146 distribution.all_distro_archive_ids),2139 self.distribution.all_distro_archive_ids),
2147 DistributionSourcePackageCache.archive == None),2140 DistributionSourcePackageCache.archive == None),
2148 DistributionSourcePackageCache.distribution == distribution,2141 DistributionSourcePackageCache.distribution ==
2142 self.distribution,
2149 ),2143 ),
2150 tables=DistributionSourcePackageCache))2144 tables=DistributionSourcePackageCache))
2151 SearchableDSPC = Table("SearchableDSPC")2145 SearchableDSPC = Table("SearchableDSPC")
@@ -2178,7 +2172,7 @@
2178 (DistributionSourcePackageInDatabase,2172 (DistributionSourcePackageInDatabase,
2179 searchable_dspc_binpkgnames),2173 searchable_dspc_binpkgnames),
2180 DistributionSourcePackageInDatabase.distribution ==2174 DistributionSourcePackageInDatabase.distribution ==
2181 distribution,2175 self.distribution,
2182 DistributionSourcePackageInDatabase.sourcepackagename_id ==2176 DistributionSourcePackageInDatabase.sourcepackagename_id ==
2183 searchable_dspc_sourcepackagename)2177 searchable_dspc_sourcepackagename)
2184 results.order_by(Desc(rank), searchable_dspc_name)2178 results.order_by(Desc(rank), searchable_dspc_name)
21852179
=== modified file 'lib/lp/services/webapp/configure.zcml'
--- lib/lp/services/webapp/configure.zcml 2014-11-28 22:07:05 +0000
+++ lib/lp/services/webapp/configure.zcml 2016-07-27 17:22:45 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2009-2011 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).
3-->3-->
44
@@ -413,6 +413,17 @@
413 permission="zope.Public"413 permission="zope.Public"
414 />414 />
415415
416 <!-- Define the widget used by fields that use
417 DistributionSourcePackageVocabulary. -->
418 <view
419 type="zope.publisher.interfaces.browser.IBrowserRequest"
420 for="zope.schema.interfaces.IChoice
421 lp.registry.vocabularies.DistributionSourcePackageVocabulary"
422 provides="zope.formlib.interfaces.IInputWidget"
423 factory="lp.app.widgets.popup.DistributionSourcePackagePickerWidget"
424 permission="zope.Public"
425 />
426
416 <!-- A simple view used by the page tests. -->427 <!-- A simple view used by the page tests. -->
417 <browser:page428 <browser:page
418 for="lp.services.webapp.interfaces.ILaunchpadRoot"429 for="lp.services.webapp.interfaces.ILaunchpadRoot"