Merge lp:~cjwatson/launchpad/potemplate-dsp-vocab into lp:launchpad

Proposed by Colin Watson on 2016-09-12
Status: Merged
Merged at revision: 18199
Proposed branch: lp:~cjwatson/launchpad/potemplate-dsp-vocab
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/productseries-ubuntupkg-dsp-vocab
Diff against target: 583 lines (+347/-28)
8 files modified
lib/lp/app/widgets/popup.py (+8/-0)
lib/lp/app/widgets/templates/distributionsourcepackage-picker.pt (+5/-0)
lib/lp/bugs/browser/widgets/bugtask.py (+1/-2)
lib/lp/bugs/doc/bugtask-package-widget.txt (+18/-21)
lib/lp/translations/browser/potemplate.py (+51/-2)
lib/lp/translations/browser/tests/test_potemplate_views.py (+68/-3)
lib/lp/translations/browser/widgets/potemplate.py (+62/-0)
lib/lp/translations/browser/widgets/tests/test_potemplate.py (+134/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/potemplate-dsp-vocab
Reviewer Review Type Date Requested Status
William Grant code 2016-09-12 Approve on 2016-09-19
Review via email: mp+305490@code.launchpad.net

Commit Message

Convert POTemplate:+edit and POTemplate:+admin to use the DistributionSourcePackage picker if the appropriate feature flag is set.

Description of the Change

Convert POTemplate:+edit and POTemplate:+admin to use the DistributionSourcePackage picker if the appropriate feature flag is set.

We need another hack in the picker setup to cope with getting the distribution from the distroseries field, and we have to be a bit more careful about how we process empty forms so that GET requests work properly, but the rest is fairly straightforward.

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-09-12 15:29:11 +0000
3+++ lib/lp/app/widgets/popup.py 2016-09-12 15:29:12 +0000
4@@ -305,6 +305,7 @@
5 return self._prefix + 'distribution'
6
7 distribution_name = ''
8+ distroseries_id = ''
9
10
11 class SourcePackageNameWidgetBase(DistributionSourcePackagePickerWidget):
12@@ -325,6 +326,13 @@
13 super(SourcePackageNameWidgetBase, self).__init__(
14 field, vocabulary, request)
15 self.cached_values = {}
16+ if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
17+ # The distribution may change later when we process form input,
18+ # but setting it here makes it easier to construct some views,
19+ # particularly edit views where we need to render the context.
20+ distribution = self.getDistribution()
21+ if distribution is not None:
22+ self.context.vocabulary.setDistribution(distribution)
23
24 def getDistribution(self):
25 """Get the distribution used for package validation.
26
27=== modified file 'lib/lp/app/widgets/templates/distributionsourcepackage-picker.pt'
28--- lib/lp/app/widgets/templates/distributionsourcepackage-picker.pt 2016-07-21 20:22:00 +0000
29+++ lib/lp/app/widgets/templates/distributionsourcepackage-picker.pt 2016-09-12 15:29:12 +0000
30@@ -9,6 +9,7 @@
31 var config = ${view/json_config};
32 var distribution_name = '${view/distribution_name}';
33 var distribution_id = '${view/distribution_id}';
34+ var distroseries_id = '${view/distroseries_id}';
35 if (distribution_name !== '') {
36 config.getContextPath = function() {
37 return '/' + distribution_name;
38@@ -17,6 +18,10 @@
39 config.getContextPath = function() {
40 return '/' + Y.DOM.byId(distribution_id).value;
41 };
42+ } else if (distroseries_id !== '') {
43+ config.getContextPath = function() {
44+ return '/' + Y.DOM.byId(distroseries_id).value.split('/', 2)[0];
45+ };
46 }
47 var show_widget_id = '${view/show_widget_id}';
48 Y.on('domready', function(e) {
49
50=== modified file 'lib/lp/bugs/browser/widgets/bugtask.py'
51--- lib/lp/bugs/browser/widgets/bugtask.py 2016-09-12 15:29:11 +0000
52+++ lib/lp/bugs/browser/widgets/bugtask.py 2016-09-12 15:29:12 +0000
53@@ -505,8 +505,7 @@
54 """See `SourcePackageNameWidgetBase`."""
55 distribution_name = self.request.form.get('field.distribution')
56 if distribution_name is None:
57- raise UnexpectedFormData(
58- "field.distribution wasn't in the request")
59+ return None
60 distribution = getUtility(IDistributionSet).getByName(
61 distribution_name)
62 if distribution is None:
63
64=== modified file 'lib/lp/bugs/doc/bugtask-package-widget.txt'
65--- lib/lp/bugs/doc/bugtask-package-widget.txt 2016-09-12 15:29:11 +0000
66+++ lib/lp/bugs/doc/bugtask-package-widget.txt 2016-09-12 15:29:12 +0000
67@@ -118,33 +118,30 @@
68 >>> request = LaunchpadTestRequest(
69 ... form={'field.distribution': 'debian',
70 ... 'field.sourcepackagename': 'linux-2.6.12'})
71- >>> widget = BugTaskAlsoAffectsSourcePackageNameWidget(
72- ... package_field, package_field.vocabulary, request)
73- >>> widget.getDistribution().name
74+ >>> BugTaskAlsoAffectsSourcePackageNameWidget(
75+ ... package_field, package_field.vocabulary,
76+ ... request).getDistribution().name
77 u'debian'
78
79-+distrotask always supplies a valid distribution name. If the widget
80-doesn't get a name, or the name isn't the name of a distro,
81-UnexpectedFormData is raised.
82++distrotask always supplies a valid distribution name or none at all. If the
83+name isn't the name of a distro, UnexpectedFormData is raised.
84
85 >>> request = LaunchpadTestRequest(
86 ... form={'field.distribution': 'non-existing',
87 ... 'field.sourcepackagename': 'linux-2.6.12'})
88- >>> widget = BugTaskAlsoAffectsSourcePackageNameWidget(
89- ... package_field, package_field.vocabulary, request)
90- >>> widget.getDistribution().name
91- Traceback (most recent call last):
92- ...
93- UnexpectedFormData: ...
94-
95- >>> request = LaunchpadTestRequest(
96- ... form={'field.sourcepackagename': 'linux-2.6.12'})
97- >>> widget = BugTaskAlsoAffectsSourcePackageNameWidget(
98- ... package_field, package_field.vocabulary, request)
99- >>> widget.getDistribution().name
100- Traceback (most recent call last):
101- ...
102- UnexpectedFormData: ...
103+ >>> BugTaskAlsoAffectsSourcePackageNameWidget(
104+ ... package_field, package_field.vocabulary,
105+ ... request).getDistribution().name
106+ Traceback (most recent call last):
107+ ...
108+ UnexpectedFormData: ...
109+
110+A GET request usually won't supply a distribution name at all.
111+
112+ >>> request = LaunchpadTestRequest(form={})
113+ >>> BugTaskAlsoAffectsSourcePackageNameWidget(
114+ ... package_field, package_field.vocabulary,
115+ ... request).getDistribution()
116
117
118 FileBugSourcePackageNameWidget
119
120=== modified file 'lib/lp/translations/browser/potemplate.py'
121--- lib/lp/translations/browser/potemplate.py 2016-06-09 20:13:17 +0000
122+++ lib/lp/translations/browser/potemplate.py 2016-09-12 15:29:12 +0000
123@@ -1,4 +1,4 @@
124-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
125+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
126 # GNU Affero General Public License version 3 (see the file LICENSE).
127 """Browser code for PO templates."""
128
129@@ -26,6 +26,7 @@
130 import operator
131 import os.path
132
133+from lazr.restful.interface import copy_field
134 from lazr.restful.utils import smartquote
135 import pytz
136 from storm.expr import (
137@@ -41,6 +42,7 @@
138 from lp import _
139 from lp.app.browser.launchpadform import (
140 action,
141+ custom_widget,
142 LaunchpadEditFormView,
143 ReturnToReferrerMixin,
144 )
145@@ -51,11 +53,15 @@
146 )
147 from lp.app.errors import NotFoundError
148 from lp.app.validators.name import valid_name
149+from lp.registry.interfaces.distributionsourcepackage import (
150+ IDistributionSourcePackage,
151+ )
152 from lp.registry.interfaces.role import IPersonRoles
153 from lp.registry.model.packaging import Packaging
154 from lp.registry.model.product import Product
155 from lp.registry.model.productseries import ProductSeries
156 from lp.registry.model.sourcepackagename import SourcePackageName
157+from lp.services.features import getFeatureFlag
158 from lp.services.helpers import is_tar_filename
159 from lp.services.propertycache import cachedproperty
160 from lp.services.webapp import (
161@@ -86,6 +92,10 @@
162 from lp.translations.browser.translationsharing import (
163 TranslationSharingDetailsMixin,
164 )
165+from lp.translations.browser.widgets.potemplate import (
166+ POTemplateAdminSourcePackageNameWidget,
167+ POTemplateEditSourcePackageNameWidget,
168+ )
169 from lp.translations.interfaces.pofile import IPOFileSet
170 from lp.translations.interfaces.potemplate import (
171 IPOTemplate,
172@@ -492,10 +502,29 @@
173 return POTemplateView.pofiles(self, preferred_only=True)
174
175
176+class IPOTemplateEditForm(IPOTemplate):
177+
178+ sourcepackagename = copy_field(
179+ IPOTemplate['sourcepackagename'],
180+ vocabularyName='DistributionSourcePackage')
181+
182+ from_sourcepackagename = copy_field(
183+ IPOTemplate['from_sourcepackagename'],
184+ vocabularyName='DistributionSourcePackage')
185+
186+
187 class POTemplateEditView(ReturnToReferrerMixin, LaunchpadEditFormView):
188 """View class that lets you edit a POTemplate object."""
189
190- schema = IPOTemplate
191+ @property
192+ def schema(self):
193+ """See `LaunchpadFormView`."""
194+ if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
195+ return IPOTemplateEditForm
196+ else:
197+ return IPOTemplate
198+
199+ custom_widget('sourcepackagename', POTemplateEditSourcePackageNameWidget)
200 label = 'Edit translation template details'
201 page_title = 'Edit details'
202 PRIORITY_MIN_VALUE = 0
203@@ -516,6 +545,14 @@
204 return field_names
205
206 @property
207+ def adapters(self):
208+ """See `LaunchpadFormView`."""
209+ if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
210+ return {IPOTemplateEditForm: self.context}
211+ else:
212+ return {}
213+
214+ @property
215 def _return_url(self):
216 # We override the ReturnToReferrerMixin _return_url because it might
217 # change when any of the productseries, distroseries,
218@@ -548,6 +585,9 @@
219 context.setActive(iscurrent)
220 old_description = context.description
221 old_translation_domain = context.translation_domain
222+ for field in ('sourcepackagename', 'from_sourcepackagename'):
223+ if IDistributionSourcePackage.providedBy(data.get(field)):
224+ data[field] = data[field].sourcepackagename
225 self.updateContextFromData(data)
226 if old_description != context.description:
227 self.user.assignKarma(
228@@ -565,6 +605,8 @@
229 """Return a POTemplateSubset corresponding to the chosen target."""
230 sourcepackagename = data.get('sourcepackagename',
231 self.context.sourcepackagename)
232+ if IDistributionSourcePackage.providedBy(sourcepackagename):
233+ sourcepackagename = sourcepackagename.sourcepackagename
234 return getUtility(IPOTemplateSet).getSubset(
235 distroseries=self.context.distroseries,
236 sourcepackagename=sourcepackagename,
237@@ -582,6 +624,8 @@
238 distroseries = data.get('distroseries', self.context.distroseries)
239 sourcepackagename = data.get(
240 'sourcepackagename', self.context.sourcepackagename)
241+ if IDistributionSourcePackage.providedBy(sourcepackagename):
242+ sourcepackagename = sourcepackagename.sourcepackagename
243 productseries = data.get('productseries', None)
244 sourcepackage_changed = (
245 distroseries is not None and
246@@ -656,6 +700,9 @@
247 'from_sourcepackagename', 'sourcepackageversion',
248 'languagepack', 'path', 'source_file_format', 'priority',
249 'date_last_updated']
250+ custom_widget('sourcepackagename', POTemplateAdminSourcePackageNameWidget)
251+ custom_widget(
252+ 'from_sourcepackagename', POTemplateAdminSourcePackageNameWidget)
253 label = 'Administer translation template'
254 page_title = "Administer"
255
256@@ -663,6 +710,8 @@
257 """Return a POTemplateSubset corresponding to the chosen target."""
258 distroseries = data.get('distroseries')
259 sourcepackagename = data.get('sourcepackagename')
260+ if IDistributionSourcePackage.providedBy(sourcepackagename):
261+ sourcepackagename = sourcepackagename.sourcepackagename
262 productseries = data.get('productseries')
263
264 if distroseries is not None and productseries is not None:
265
266=== modified file 'lib/lp/translations/browser/tests/test_potemplate_views.py'
267--- lib/lp/translations/browser/tests/test_potemplate_views.py 2012-12-10 13:43:47 +0000
268+++ lib/lp/translations/browser/tests/test_potemplate_views.py 2016-09-12 15:29:12 +0000
269@@ -1,25 +1,46 @@
270-# Copyright 2011 Canonical Ltd. This software is licensed under the
271+# Copyright 2011-2016 Canonical Ltd. This software is licensed under the
272 # GNU Affero General Public License version 3 (see the file LICENSE).
273
274 """Module doc."""
275
276 __metaclass__ = type
277
278+from testscenarios import (
279+ load_tests_apply_scenarios,
280+ WithScenarios,
281+ )
282
283+from lp.services.features.testing import FeatureFixture
284 from lp.services.webapp.escaping import html_escape
285 from lp.services.webapp.servers import LaunchpadTestRequest
286-from lp.testing import TestCaseWithFactory
287+from lp.testing import (
288+ celebrity_logged_in,
289+ TestCaseWithFactory,
290+ )
291 from lp.testing.layers import DatabaseFunctionalLayer
292+from lp.testing.views import create_initialized_view
293 from lp.translations.browser.potemplate import (
294 POTemplateAdminView,
295 POTemplateEditView,
296 )
297
298
299-class TestPOTemplateEditViewValidation(TestCaseWithFactory):
300+class TestPOTemplateEditViewValidation(WithScenarios, TestCaseWithFactory):
301
302 layer = DatabaseFunctionalLayer
303
304+ scenarios = [
305+ ("spn_picker", {"features": {}}),
306+ ("dsp_picker", {
307+ "features": {u"disclosure.dsp_picker.enabled": u"on"},
308+ }),
309+ ]
310+
311+ def setUp(self):
312+ super(TestPOTemplateEditViewValidation, self).setUp()
313+ if self.features:
314+ self.useFixture(FeatureFixture(self.features))
315+
316 def _makeData(self, potemplate, **kwargs):
317 """Create form data for the given template with some changed values.
318
319@@ -131,6 +152,23 @@
320 [u'Source package already has a template with that same domain.'],
321 view.errors)
322
323+ def test_change_sourcepackage(self):
324+ # Changing the source package is honoured.
325+ distroseries = self.factory.makeDistroSeries()
326+ potemplate = self.factory.makePOTemplate(distroseries=distroseries)
327+ dsp = self.factory.makeDSPCache(distroseries=distroseries)
328+ form = {
329+ 'field.name': potemplate.name,
330+ 'field.distroseries': distroseries.name,
331+ 'field.sourcepackagename': dsp.sourcepackagename.name,
332+ 'field.actions.change': 'Change',
333+ }
334+ with celebrity_logged_in('rosetta_experts'):
335+ view = create_initialized_view(potemplate, '+edit', form=form)
336+ self.assertEqual([], view.errors)
337+ self.assertEqual(
338+ dsp.sourcepackagename.name, potemplate.sourcepackagename.name)
339+
340
341 class TestPOTemplateAdminViewValidation(TestPOTemplateEditViewValidation):
342
343@@ -194,3 +232,30 @@
344 self.assertEqual(
345 [u'Choose a distribution release series or a project '
346 u'release series, but not both.'], view.errors)
347+
348+ def test_change_from_sourcepackage(self):
349+ # Changing the source package the template comes from is honoured.
350+ distroseries = self.factory.makeDistroSeries()
351+ dsp = self.factory.makeDSPCache(distroseries=distroseries)
352+ potemplate = self.factory.makePOTemplate(
353+ distroseries=distroseries, sourcepackagename=dsp.sourcepackagename)
354+ from_dsp = self.factory.makeDSPCache(distroseries=distroseries)
355+ form = {
356+ 'field.name': potemplate.name,
357+ 'field.distroseries': '%s/%s' % (
358+ distroseries.distribution.name, distroseries.name),
359+ 'field.sourcepackagename': dsp.sourcepackagename.name,
360+ 'field.from_sourcepackagename': from_dsp.sourcepackagename.name,
361+ 'field.actions.change': 'Change',
362+ }
363+ with celebrity_logged_in('rosetta_experts'):
364+ view = create_initialized_view(potemplate, '+admin', form=form)
365+ self.assertEqual([], view.errors)
366+ self.assertEqual(
367+ dsp.sourcepackagename.name, potemplate.sourcepackagename.name)
368+ self.assertEqual(
369+ from_dsp.sourcepackagename.name,
370+ potemplate.from_sourcepackagename.name)
371+
372+
373+load_tests = load_tests_apply_scenarios
374
375=== added directory 'lib/lp/translations/browser/widgets'
376=== added file 'lib/lp/translations/browser/widgets/__init__.py'
377=== added file 'lib/lp/translations/browser/widgets/potemplate.py'
378--- lib/lp/translations/browser/widgets/potemplate.py 1970-01-01 00:00:00 +0000
379+++ lib/lp/translations/browser/widgets/potemplate.py 2016-09-12 15:29:12 +0000
380@@ -0,0 +1,62 @@
381+# Copyright 2016 Canonical Ltd. This software is licensed under the
382+# GNU Affero General Public License version 3 (see the file LICENSE).
383+
384+"""Widgets related to `IPOTemplate`."""
385+
386+from __future__ import absolute_import, print_function, unicode_literals
387+
388+__metaclass__ = type
389+__all__ = [
390+ "POTemplateAdminSourcePackageNameWidget",
391+ "POTemplateEditSourcePackageNameWidget",
392+ ]
393+
394+from lp.app.errors import UnexpectedFormData
395+from lp.app.widgets.popup import SourcePackageNameWidgetBase
396+from lp.registry.vocabularies import DistroSeriesVocabulary
397+
398+
399+class POTemplateEditSourcePackageNameWidget(SourcePackageNameWidgetBase):
400+ """A widget for associating a POTemplate with a SourcePackageName.
401+
402+ This is suitable for use on POTemplate:+edit, where the distribution is
403+ fixed.
404+ """
405+
406+ @property
407+ def distribution_name(self):
408+ distribution = self.getDistribution()
409+ if distribution is not None:
410+ return distribution.name
411+ else:
412+ return ''
413+
414+ def getDistribution(self):
415+ """See `SourcePackageNameWidgetBase`."""
416+ return self.context.context.distribution
417+
418+
419+class POTemplateAdminSourcePackageNameWidget(SourcePackageNameWidgetBase):
420+ """A widget for associating a POTemplate with a SourcePackageName.
421+
422+ This is suitable for use on POTemplate:+admin, where the distribution
423+ may be changed via the distroseries field.
424+ """
425+
426+ @property
427+ def distroseries_id(self):
428+ return self._prefix + 'distroseries'
429+
430+ def getDistribution(self):
431+ """See `SourcePackageNameWidgetBase`."""
432+ distroseries_token = self.request.form.get('field.distroseries')
433+ if distroseries_token is None:
434+ # Fall back to the POTemplate's current distribution.
435+ return self.context.context.distribution
436+ distroseries_vocab = DistroSeriesVocabulary()
437+ try:
438+ term = distroseries_vocab.getTermByToken(distroseries_token)
439+ except LookupError:
440+ raise UnexpectedFormData(
441+ "No such distribution series: %s" % distroseries_token)
442+ return term.value.distribution
443
444=== added directory 'lib/lp/translations/browser/widgets/tests'
445=== added file 'lib/lp/translations/browser/widgets/tests/__init__.py'
446=== added file 'lib/lp/translations/browser/widgets/tests/test_potemplate.py'
447--- lib/lp/translations/browser/widgets/tests/test_potemplate.py 1970-01-01 00:00:00 +0000
448+++ lib/lp/translations/browser/widgets/tests/test_potemplate.py 2016-09-12 15:29:12 +0000
449@@ -0,0 +1,134 @@
450+# Copyright 2016 Canonical Ltd. This software is licensed under the
451+# GNU Affero General Public License version 3 (see the file LICENSE).
452+
453+"""Test the POTemplate widgets."""
454+
455+from __future__ import absolute_import, print_function, unicode_literals
456+
457+__metaclass__ = type
458+
459+from testscenarios import (
460+ load_tests_apply_scenarios,
461+ WithScenarios,
462+ )
463+
464+from lp.app.errors import UnexpectedFormData
465+from lp.services.features.testing import FeatureFixture
466+from lp.services.webapp.servers import LaunchpadTestRequest
467+from lp.testing import TestCaseWithFactory
468+from lp.testing.layers import DatabaseFunctionalLayer
469+from lp.translations.browser.potemplate import IPOTemplateEditForm
470+from lp.translations.browser.widgets.potemplate import (
471+ POTemplateAdminSourcePackageNameWidget,
472+ POTemplateEditSourcePackageNameWidget,
473+ )
474+from lp.translations.interfaces.potemplate import IPOTemplate
475+
476+
477+class TestPOTemplateEditSourcePackageNameWidget(
478+ WithScenarios, TestCaseWithFactory):
479+
480+ layer = DatabaseFunctionalLayer
481+
482+ scenarios = [
483+ ("spn_picker", {
484+ "features": {},
485+ "interface": IPOTemplate,
486+ }),
487+ ("dsp_picker", {
488+ "features": {u"disclosure.dsp_picker.enabled": u"on"},
489+ "interface": IPOTemplateEditForm,
490+ }),
491+ ]
492+
493+ def setUp(self):
494+ super(TestPOTemplateEditSourcePackageNameWidget, self).setUp()
495+ if self.features:
496+ self.useFixture(FeatureFixture(self.features))
497+
498+ def makeWidget(self, potemplate, form=None):
499+ field = self.interface["sourcepackagename"]
500+ bound_field = field.bind(potemplate)
501+ request = LaunchpadTestRequest(form=form)
502+ return POTemplateEditSourcePackageNameWidget(
503+ bound_field, bound_field.vocabulary, request)
504+
505+ def test_productseries(self):
506+ potemplate = self.factory.makePOTemplate()
507+ widget = self.makeWidget(potemplate)
508+ self.assertIsNone(widget.getDistribution())
509+ self.assertEqual("", widget.distribution_name)
510+
511+ def test_distroseries(self):
512+ distroseries = self.factory.makeDistroSeries()
513+ potemplate = self.factory.makePOTemplate(distroseries=distroseries)
514+ widget = self.makeWidget(potemplate)
515+ self.assertEqual(distroseries.distribution, widget.getDistribution())
516+ self.assertEqual(
517+ distroseries.distribution.name, widget.distribution_name)
518+
519+
520+class TestPOTemplateAdminSourcePackageNameWidget(
521+ WithScenarios, TestCaseWithFactory):
522+
523+ layer = DatabaseFunctionalLayer
524+
525+ scenarios = [
526+ ("spn_picker", {
527+ "features": {},
528+ "interface": IPOTemplate,
529+ }),
530+ ("dsp_picker", {
531+ "features": {u"disclosure.dsp_picker.enabled": u"on"},
532+ "interface": IPOTemplateEditForm,
533+ }),
534+ ]
535+
536+ def setUp(self):
537+ super(TestPOTemplateAdminSourcePackageNameWidget, self).setUp()
538+ if self.features:
539+ self.useFixture(FeatureFixture(self.features))
540+
541+ def makeWidget(self, potemplate, form=None):
542+ field = self.interface["sourcepackagename"]
543+ bound_field = field.bind(potemplate)
544+ request = LaunchpadTestRequest(form=form)
545+ return POTemplateAdminSourcePackageNameWidget(
546+ bound_field, bound_field.vocabulary, request)
547+
548+ def test_distroseries_id(self):
549+ potemplate = self.factory.makePOTemplate()
550+ distroseries = self.factory.makeDistroSeries()
551+ form = {
552+ "field.distroseries": "%s/%s" % (
553+ distroseries.distribution.name, distroseries.name),
554+ }
555+ widget = self.makeWidget(potemplate, form=form)
556+ self.assertEqual("field.distroseries", widget.distroseries_id)
557+
558+ def test_getDistribution(self):
559+ potemplate = self.factory.makePOTemplate()
560+ distroseries = self.factory.makeDistroSeries()
561+ form = {
562+ "field.distroseries": "%s/%s" % (
563+ distroseries.distribution.name, distroseries.name),
564+ }
565+ widget = self.makeWidget(potemplate, form=form)
566+ self.assertEqual(distroseries.distribution, widget.getDistribution())
567+
568+ def test_getDistribution_missing_field(self):
569+ distroseries = self.factory.makeDistroSeries()
570+ potemplate = self.factory.makePOTemplate(distroseries=distroseries)
571+ widget = self.makeWidget(potemplate, form={})
572+ self.assertEqual(distroseries.distribution, widget.getDistribution())
573+
574+ def test_getDistribution_non_existent_distroseries(self):
575+ potemplate = self.factory.makePOTemplate()
576+ form = {"field.distroseries": "not-a-distribution/not-a-series"}
577+ self.assertRaises(
578+ UnexpectedFormData,
579+ lambda: (
580+ self.makeWidget(potemplate, form=form).getDistribution().name))
581+
582+
583+load_tests = load_tests_apply_scenarios