Merge lp:~cjwatson/launchpad/productseries-ubuntupkg-dsp-vocab into lp:launchpad

Proposed by Colin Watson on 2016-09-09
Status: Merged
Merged at revision: 18195
Proposed branch: lp:~cjwatson/launchpad/productseries-ubuntupkg-dsp-vocab
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/distribution-filebug-dsp-vocab
Diff against target: 526 lines (+164/-86)
7 files modified
lib/lp/app/widgets/popup.py (+85/-1)
lib/lp/bugs/browser/bugtracker.py (+2/-2)
lib/lp/bugs/browser/widgets/bugtask.py (+11/-74)
lib/lp/registry/browser/productseries.py (+33/-4)
lib/lp/registry/browser/tests/test_packaging.py (+20/-3)
lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py (+11/-0)
lib/lp/registry/vocabularies.py (+2/-2)
To merge this branch: bzr merge lp:~cjwatson/launchpad/productseries-ubuntupkg-dsp-vocab
Reviewer Review Type Date Requested Status
William Grant code 2016-09-09 Approve on 2016-09-19
Review via email: mp+305367@code.launchpad.net

Commit Message

Convert ProductSeries:+ubuntupkg to use the DistributionSourcePackage picker if the appropriate feature flag is set.

Description of the Change

Convert ProductSeries:+ubuntupkg to use the DistributionSourcePackage picker if the appropriate feature flag is set.

I had to move most of BugTaskSourcePackageNameWidget into somewhat more common code to achieve this, since this view just asks for the source package name within Ubuntu rather than anything more generic. This will also be useful for the remaining views that need similar work after this (POTemplate:+edit, POTemplate:+admin, and TranslationImportQueueEntry:+index).

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/widgets/popup.py'
2--- lib/lp/app/widgets/popup.py 2016-07-21 20:22:00 +0000
3+++ lib/lp/app/widgets/popup.py 2016-09-09 17:05:20 +0000
4@@ -4,21 +4,38 @@
5 """Single selection widget using a popup to select one item from many."""
6
7 __metaclass__ = type
8+__all__ = [
9+ "BugTrackerPickerWidget",
10+ "DistributionSourcePackagePickerWidget",
11+ "PersonPickerWidget",
12+ "SearchForUpstreamPopupWidget",
13+ "SourcePackageNameWidgetBase",
14+ "UbuntuSourcePackageNameWidget",
15+ "VocabularyPickerWidget",
16+ ]
17
18 from lazr.restful.utils import safe_hasattr
19 import simplejson
20 from z3c.ptcompat import ViewPageTemplateFile
21+from zope.component import getUtility
22+from zope.formlib.interfaces import ConversionError
23 from zope.formlib.itemswidgets import (
24 ItemsWidgetBase,
25 SingleDataHelper,
26 )
27-from zope.schema.interfaces import IChoice
28+from zope.schema.interfaces import (
29+ IChoice,
30+ InvalidValue,
31+ )
32
33 from lp.app.browser.stringformatter import FormattersAPI
34 from lp.app.browser.vocabulary import (
35 get_person_picker_entry_metadata,
36 vocabulary_filters,
37 )
38+from lp.app.errors import NotFoundError
39+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
40+from lp.services.features import getFeatureFlag
41 from lp.services.propertycache import cachedproperty
42 from lp.services.webapp import canonical_url
43 from lp.services.webapp.escaping import structured
44@@ -288,3 +305,70 @@
45 return self._prefix + 'distribution'
46
47 distribution_name = ''
48+
49+
50+class SourcePackageNameWidgetBase(DistributionSourcePackagePickerWidget):
51+ """A base widget for choosing a SourcePackageName.
52+
53+ It accepts both binary and source package names. Widgets inheriting
54+ from this must implement `getDistribution`.
55+
56+ Most views should use LaunchpadTargetWidget or
57+ DistributionSourcePackagePickerWidget instead, but this is useful in
58+ cases where the distribution is fixed in some other way.
59+ """
60+
61+ # Pages that use this widget don't display the distribution.
62+ distribution_id = ''
63+
64+ def __init__(self, field, vocabulary, request):
65+ super(SourcePackageNameWidgetBase, self).__init__(
66+ field, vocabulary, request)
67+ self.cached_values = {}
68+
69+ def getDistribution(self):
70+ """Get the distribution used for package validation.
71+
72+ The package name has to be published in the returned distribution.
73+ """
74+ raise NotImplementedError
75+
76+ def _toFieldValue(self, input):
77+ if not input:
78+ return self.context.missing_value
79+
80+ distribution = self.getDistribution()
81+ cached_value = self.cached_values.get(input)
82+ if cached_value:
83+ return cached_value
84+ if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
85+ try:
86+ self.context.vocabulary.setDistribution(distribution)
87+ return self.context.vocabulary.getTermByToken(input).value
88+ except LookupError:
89+ raise ConversionError(
90+ "Launchpad doesn't know of any source package named"
91+ " '%s' in %s." % (input, distribution.displayname))
92+ # Else the untrusted SPN vocab was used so it needs secondary
93+ # verification.
94+ try:
95+ source = distribution.guessPublishedSourcePackageName(input)
96+ except NotFoundError:
97+ try:
98+ source = self.convertTokensToValues([input])[0]
99+ except InvalidValue:
100+ raise ConversionError(
101+ "Launchpad doesn't know of any source package named"
102+ " '%s' in %s." % (input, distribution.displayname))
103+ self.cached_values[input] = source
104+ return source
105+
106+
107+class UbuntuSourcePackageNameWidget(SourcePackageNameWidgetBase):
108+ """A widget to select Ubuntu packages."""
109+
110+ distribution_name = 'ubuntu'
111+
112+ def getDistribution(self):
113+ """See `SourcePackageNameWidgetBase`"""
114+ return getUtility(ILaunchpadCelebrities).ubuntu
115
116=== modified file 'lib/lp/bugs/browser/bugtracker.py'
117--- lib/lp/bugs/browser/bugtracker.py 2015-09-28 17:38:45 +0000
118+++ lib/lp/bugs/browser/bugtracker.py 2016-09-09 17:05:20 +0000
119@@ -1,4 +1,4 @@
120-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
121+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
122 # GNU Affero General Public License version 3 (see the file LICENSE).
123
124 """Bug tracker views."""
125@@ -40,8 +40,8 @@
126 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
127 from lp.app.validators import LaunchpadValidationError
128 from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
129+from lp.app.widgets.popup import UbuntuSourcePackageNameWidget
130 from lp.app.widgets.textwidgets import DelimitedListWidget
131-from lp.bugs.browser.widgets.bugtask import UbuntuSourcePackageNameWidget
132 from lp.bugs.interfaces.bugtracker import (
133 BugTrackerType,
134 IBugTracker,
135
136=== modified file 'lib/lp/bugs/browser/widgets/bugtask.py'
137--- lib/lp/bugs/browser/widgets/bugtask.py 2016-09-09 17:05:19 +0000
138+++ lib/lp/bugs/browser/widgets/bugtask.py 2016-09-09 17:05:20 +0000
139@@ -15,13 +15,11 @@
140 "DBItemDisplayWidget",
141 "FileBugSourcePackageNameWidget",
142 "NewLineToSpacesWidget",
143- "UbuntuSourcePackageNameWidget",
144 ]
145
146 from z3c.ptcompat import ViewPageTemplateFile
147 from zope.component import getUtility
148 from zope.formlib.interfaces import (
149- ConversionError,
150 IDisplayWidget,
151 IInputWidget,
152 InputErrors,
153@@ -39,24 +37,17 @@
154 implementer,
155 Interface,
156 )
157-from zope.schema.interfaces import (
158- InvalidValue,
159- ValidationError,
160- )
161+from zope.schema.interfaces import ValidationError
162 from zope.schema.vocabulary import getVocabularyRegistry
163
164 from lp import _
165 from lp.app.browser.tales import TeamFormatterAPI
166-from lp.app.errors import (
167- NotFoundError,
168- UnexpectedFormData,
169- )
170-from lp.app.interfaces.launchpad import ILaunchpadCelebrities
171+from lp.app.errors import UnexpectedFormData
172 from lp.app.widgets.helpers import get_widget_template
173 from lp.app.widgets.launchpadtarget import LaunchpadTargetWidget
174 from lp.app.widgets.popup import (
175- DistributionSourcePackagePickerWidget,
176 PersonPickerWidget,
177+ SourcePackageNameWidgetBase,
178 )
179 from lp.app.widgets.textwidgets import (
180 StrippedTextWidget,
181@@ -483,27 +474,14 @@
182 return vocabulary
183
184
185-class BugTaskSourcePackageNameWidget(DistributionSourcePackagePickerWidget):
186+class BugTaskSourcePackageNameWidget(SourcePackageNameWidgetBase):
187 """A widget for associating a bugtask with a SourcePackageName.
188
189 It accepts both binary and source package names.
190 """
191
192- # Pages that use this widget don't display the distribution, but this
193- # can only be used by bugtasks on the distribution in question so the
194- # vocabulary will be able to work it out for itself.
195- distribution_id = ''
196-
197- def __init__(self, field, vocabulary, request):
198- super(BugTaskSourcePackageNameWidget, self).__init__(
199- field, vocabulary, request)
200- self.cached_values = {}
201-
202 def getDistribution(self):
203- """Get the distribution used for package validation.
204-
205- The package name has to be published in the returned distribution.
206- """
207+ """See `SourcePackageNameWidgetBase`."""
208 field = self.context
209 distribution = field.context.distribution
210 if distribution is None and field.context.distroseries is not None:
211@@ -513,39 +491,8 @@
212 " bugtasks on distributions or on distribution series.")
213 return distribution
214
215- def _toFieldValue(self, input):
216- if not input:
217- return self.context.missing_value
218-
219- distribution = self.getDistribution()
220- cached_value = self.cached_values.get(input)
221- if cached_value:
222- return cached_value
223- if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
224- try:
225- self.context.vocabulary.setDistribution(distribution)
226- return self.context.vocabulary.getTermByToken(input).value
227- except LookupError:
228- raise ConversionError(
229- "Launchpad doesn't know of any source package named"
230- " '%s' in %s." % (input, distribution.displayname))
231- # Else the untrusted SPN vocab was used so it needs secondary
232- # verification.
233- try:
234- source = distribution.guessPublishedSourcePackageName(input)
235- except NotFoundError:
236- try:
237- source = self.convertTokensToValues([input])[0]
238- except InvalidValue:
239- raise ConversionError(
240- "Launchpad doesn't know of any source package named"
241- " '%s' in %s." % (input, distribution.displayname))
242- self.cached_values[input] = source
243- return source
244-
245-
246-class BugTaskAlsoAffectsSourcePackageNameWidget(
247- BugTaskSourcePackageNameWidget):
248+
249+class BugTaskAlsoAffectsSourcePackageNameWidget(SourcePackageNameWidgetBase):
250 """Package widget for +distrotask.
251
252 This widget works the same as `BugTaskSourcePackageNameWidget`, except
253@@ -555,7 +502,7 @@
254 distribution_id = 'field.distribution'
255
256 def getDistribution(self):
257- """See `BugTaskSourcePackageNameWidget`."""
258+ """See `SourcePackageNameWidgetBase`."""
259 distribution_name = self.request.form.get('field.distribution')
260 if distribution_name is None:
261 raise UnexpectedFormData(
262@@ -568,7 +515,7 @@
263 return distribution
264
265
266-class FileBugSourcePackageNameWidget(BugTaskSourcePackageNameWidget):
267+class FileBugSourcePackageNameWidget(SourcePackageNameWidgetBase):
268 """Package widget for +filebug.
269
270 This widget works the same as `BugTaskSourcePackageNameWidget`, except
271@@ -577,7 +524,7 @@
272 """
273
274 def getDistribution(self):
275- """See `BugTaskSourcePackageNameWidget`."""
276+ """See `SourcePackageNameWidgetBase`."""
277 field = self.context
278 pillar = field.context.pillar
279 assert IDistribution.providedBy(pillar), (
280@@ -586,7 +533,7 @@
281 return pillar
282
283 def _toFieldValue(self, input):
284- """See `BugTaskSourcePackageNameWidget`."""
285+ """See `SourcePackageNameWidgetBase`."""
286 source = super(FileBugSourcePackageNameWidget, self)._toFieldValue(
287 input)
288 if (source is not None and
289@@ -604,16 +551,6 @@
290 return source
291
292
293-class UbuntuSourcePackageNameWidget(BugTaskSourcePackageNameWidget):
294- """A widget to select Ubuntu packages."""
295-
296- distribution_name = 'ubuntu'
297-
298- def getDistribution(self):
299- """See `BugTaskSourcePackageNameWidget`"""
300- return getUtility(ILaunchpadCelebrities).ubuntu
301-
302-
303 @implementer(IDisplayWidget)
304 class AssigneeDisplayWidget(BrowserWidget):
305 """A widget for displaying an assignee."""
306
307=== modified file 'lib/lp/registry/browser/productseries.py'
308--- lib/lp/registry/browser/productseries.py 2015-09-29 00:05:21 +0000
309+++ lib/lp/registry/browser/productseries.py 2016-09-09 17:05:20 +0000
310@@ -1,4 +1,4 @@
311-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
312+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
313 # GNU Affero General Public License version 3 (see the file LICENSE).
314
315 """View classes for `IProductSeries`."""
316@@ -28,6 +28,7 @@
317
318 from operator import attrgetter
319
320+from lazr.restful.interface import copy_field
321 from z3c.ptcompat import ViewPageTemplateFile
322 from zope.component import getUtility
323 from zope.formlib import form
324@@ -57,6 +58,7 @@
325 from lp.app.enums import ServiceUsage
326 from lp.app.errors import NotFoundError
327 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
328+from lp.app.widgets.popup import UbuntuSourcePackageNameWidget
329 from lp.app.widgets.textwidgets import StrippedTextWidget
330 from lp.blueprints.browser.specificationtarget import (
331 HasSpecificationsMenuMixin,
332@@ -86,6 +88,9 @@
333 from lp.registry.browser.product import ProductSetBranchView
334 from lp.registry.enums import VCSType
335 from lp.registry.errors import CannotPackageProprietaryProduct
336+from lp.registry.interfaces.distributionsourcepackage import (
337+ IDistributionSourcePackage,
338+ )
339 from lp.registry.interfaces.packaging import (
340 IPackaging,
341 IPackagingUtil,
342@@ -93,6 +98,7 @@
343 from lp.registry.interfaces.productseries import IProductSeries
344 from lp.registry.interfaces.series import SeriesStatus
345 from lp.services.config import config
346+from lp.services.features import getFeatureFlag
347 from lp.services.propertycache import cachedproperty
348 from lp.services.webapp import (
349 ApplicationMenu,
350@@ -482,10 +488,25 @@
351 return list(self.context.releases[:12])
352
353
354+class IPackagingForm(IPackaging):
355+
356+ sourcepackagename = copy_field(
357+ IPackaging['sourcepackagename'],
358+ vocabularyName='DistributionSourcePackage')
359+
360+
361 class ProductSeriesUbuntuPackagingView(LaunchpadFormView):
362
363- schema = IPackaging
364+ @property
365+ def schema(self):
366+ """See `LaunchpadFormView`."""
367+ if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
368+ return IPackagingForm
369+ else:
370+ return IPackaging
371+
372 field_names = ['sourcepackagename', 'distroseries']
373+ custom_widget('sourcepackagename', UbuntuSourcePackageNameWidget)
374 page_title = 'Ubuntu source packaging'
375 label = page_title
376
377@@ -497,7 +518,11 @@
378 self._ubuntu_series = self._ubuntu.currentseries
379 try:
380 package = self.context.getPackage(self._ubuntu_series)
381- self.default_sourcepackagename = package.sourcepackagename
382+ if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
383+ self.default_sourcepackagename = self._ubuntu.getSourcePackage(
384+ package.sourcepackagename)
385+ else:
386+ self.default_sourcepackagename = package.sourcepackagename
387 except NotFoundError:
388 # The package has never been set.
389 self.default_sourcepackagename = None
390@@ -555,6 +580,8 @@
391 def validate(self, data):
392 productseries = self.context
393 sourcepackagename = data.get('sourcepackagename', None)
394+ if IDistributionSourcePackage.providedBy(sourcepackagename):
395+ sourcepackagename = sourcepackagename.sourcepackagename
396 distroseries = self._getSubmittedSeries(data)
397
398 packaging_util = getUtility(IPackagingUtil)
399@@ -595,6 +622,8 @@
400 # ubuntu series. if none exists, one will be created
401 distroseries = self._getSubmittedSeries(data)
402 sourcepackagename = data['sourcepackagename']
403+ if IDistributionSourcePackage.providedBy(sourcepackagename):
404+ sourcepackagename = sourcepackagename.sourcepackagename
405 if getUtility(IPackagingUtil).packagingEntryExists(
406 sourcepackagename, distroseries, productseries=self.context):
407 # There is no change.
408@@ -602,7 +631,7 @@
409 try:
410 self.context.setPackaging(
411 distroseries, sourcepackagename, self.user)
412- except CannotPackageProprietaryProduct, e:
413+ except CannotPackageProprietaryProduct as e:
414 self.request.response.addErrorNotification(str(e))
415
416
417
418=== modified file 'lib/lp/registry/browser/tests/test_packaging.py'
419--- lib/lp/registry/browser/tests/test_packaging.py 2016-06-17 15:46:57 +0000
420+++ lib/lp/registry/browser/tests/test_packaging.py 2016-09-09 17:05:20 +0000
421@@ -5,6 +5,10 @@
422
423 __metaclass__ = type
424
425+from testscenarios import (
426+ load_tests_apply_scenarios,
427+ WithScenarios,
428+ )
429 from zope.component import getUtility
430
431 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
432@@ -15,6 +19,7 @@
433 )
434 from lp.registry.interfaces.product import IProductSet
435 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
436+from lp.services.features.testing import FeatureFixture
437 from lp.testing import (
438 login,
439 logout,
440@@ -28,13 +33,22 @@
441 from lp.testing.views import create_initialized_view
442
443
444-class TestProductSeriesUbuntuPackagingView(TestCaseWithFactory):
445- """Browser tests for deletion of Packaging objects."""
446+class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
447+ """Browser tests for adding Packaging objects."""
448
449 layer = DatabaseFunctionalLayer
450
451+ scenarios = [
452+ ("spn_picker", {"features": {}}),
453+ ("dsp_picker", {
454+ "features": {u"disclosure.dsp_picker.enabled": u"on"},
455+ }),
456+ ]
457+
458 def setUp(self):
459 super(TestProductSeriesUbuntuPackagingView, self).setUp()
460+ if self.features:
461+ self.useFixture(FeatureFixture(self.features))
462 self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
463 self.hoary = self.ubuntu.getSeries('hoary')
464 self.sourcepackagename = self.factory.makeSourcePackageName('hot')
465@@ -88,7 +102,7 @@
466 'hot</a> package in Hoary is already linked to another series.']
467 self.assertEqual(view_errors, view.errors)
468
469- def test_sourcepackgename_required(self):
470+ def test_sourcepackagename_required(self):
471 # A source package name must be provided.
472 form = {
473 'field.distroseries': 'hoary',
474@@ -188,3 +202,6 @@
475 productseries=productseries,
476 sourcepackagename=package_name,
477 distroseries=distroseries))
478+
479+
480+load_tests = load_tests_apply_scenarios
481
482=== modified file 'lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py'
483--- lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py 2016-09-09 17:05:19 +0000
484+++ lib/lp/registry/tests/test_distributionsourcepackage_vocabulary.py 2016-09-09 17:05:20 +0000
485@@ -175,6 +175,17 @@
486 self.assertEqual(dsp.name, term.title)
487 self.assertEqual(dsp, term.value)
488
489+ def test_toTerm_dsp_no_distribution(self):
490+ # The vocabulary can convert a DSP to a term even if it does not yet
491+ # have a distribution.
492+ dsp = self.factory.makeDistributionSourcePackage(
493+ sourcepackagename='foo', with_db=False)
494+ vocabulary = DistributionSourcePackageVocabulary(None)
495+ term = vocabulary.toTerm(dsp)
496+ self.assertEqual(dsp.name, term.token)
497+ self.assertEqual(dsp.name, term.title)
498+ self.assertEqual(dsp, term.value)
499+
500 def test_getTermByToken_error(self):
501 # An error is raised if the token does not match a official DSP.
502 distro = self.factory.makeDistribution()
503
504=== modified file 'lib/lp/registry/vocabularies.py'
505--- lib/lp/registry/vocabularies.py 2016-09-09 17:05:19 +0000
506+++ lib/lp/registry/vocabularies.py 2016-09-09 17:05:20 +0000
507@@ -2073,7 +2073,6 @@
508
509 def toTerm(self, spn_or_dsp):
510 """See `IVocabulary`."""
511- self._assertHasDistribution()
512 dsp = None
513 binary_names = None
514 if isinstance(spn_or_dsp, tuple):
515@@ -2088,9 +2087,10 @@
516 if IDistributionSourcePackage.providedBy(spn_or_dsp):
517 dsp = spn_or_dsp
518 elif spn_or_dsp is not None:
519+ self._assertHasDistribution()
520 dsp = self.distribution.getSourcePackage(spn_or_dsp)
521 if dsp is not None:
522- if dsp == self.dsp or dsp.is_official:
523+ if dsp == self.dsp or dsp.is_official or self.distribution is None:
524 if binary_names:
525 # Search already did the hard work of looking up binary
526 # names.