Merge ~cjwatson/launchpad:merge-snap-build-channels-widgets into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 49867997e59c81dbd872332d99991f3b01a4ba50
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:merge-snap-build-channels-widgets
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:snap-build-channels-field
Diff against target: 990 lines (+165/-283)
7 files modified
dev/null (+0/-215)
lib/lp/app/widgets/snapbuildchannels.py (+14/-17)
lib/lp/app/widgets/templates/snapbuildchannels.pt (+12/-0)
lib/lp/app/widgets/tests/test_snapbuildchannels.py (+28/-40)
lib/lp/charms/browser/charmrecipe.py (+23/-6)
lib/lp/snappy/browser/snap.py (+32/-4)
lib/lp/snappy/browser/tests/test_snap.py (+56/-1)
Reviewer Review Type Date Requested Status
Ines Almeida Approve
Review via email: mp+411628@code.launchpad.net

Commit message

Use a common SnapBuildChannelsWidget for snap and charm recipes

Description of the change

These were previously very similar, so consolidate them into common code. They now pick up the list of snap names to offer from the context field's vocabulary, so there is now exactly one place (`SnapBuildChannelsField._core_snap_names`) that knows about the list of valid core snap names.

To post a comment you must log in.
Revision history for this message
Ines Almeida (ines-almeida) wrote :

LGTM, just a small comment

review: Approve
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Ines Almeida (ines-almeida) wrote :

No further comments after looking at the individual commits

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py b/lib/lp/app/widgets/snapbuildchannels.py
2similarity index 82%
3rename from lib/lp/charms/browser/widgets/charmrecipebuildchannels.py
4rename to lib/lp/app/widgets/snapbuildchannels.py
5index 5a15feb..d1da50b 100644
6--- a/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py
7+++ b/lib/lp/app/widgets/snapbuildchannels.py
8@@ -1,10 +1,10 @@
9-# Copyright 2021 Canonical Ltd. This software is licensed under the
10+# Copyright 2018-2021 Canonical Ltd. This software is licensed under the
11 # GNU Affero General Public License version 3 (see the file LICENSE).
12
13-"""A widget for selecting source snap channels for charm recipe builds."""
14+"""A widget for selecting source snap channels for builds."""
15
16 __all__ = [
17- "CharmRecipeBuildChannelsWidget",
18+ "SnapBuildChannelsWidget",
19 ]
20
21 from zope.browserpage import ViewPageTemplateFile
22@@ -23,19 +23,15 @@ from lp.services.webapp.interfaces import (
23
24
25 @implementer(ISingleLineWidgetLayout, IAlwaysSubmittedWidget, IInputWidget)
26-class CharmRecipeBuildChannelsWidget(BrowserWidget, InputWidget):
27+class SnapBuildChannelsWidget(BrowserWidget, InputWidget):
28
29- template = ViewPageTemplateFile("templates/charmrecipebuildchannels.pt")
30+ template = ViewPageTemplateFile("templates/snapbuildchannels.pt")
31 hint = False
32- snap_names = ["charmcraft", "core", "core18", "core20", "core22"]
33 _widgets_set_up = False
34
35- def __init__(self, context, request):
36- super().__init__(context, request)
37- self.hint = (
38- "The channels to use for build tools when building the charm "
39- "recipe."
40- )
41+ @property
42+ def snap_names(self):
43+ return sorted(term.value for term in self.context.key_type.vocabulary)
44
45 def setUpSubWidgets(self):
46 if self._widgets_set_up:
47@@ -52,6 +48,10 @@ class CharmRecipeBuildChannelsWidget(BrowserWidget, InputWidget):
48 setUpWidget(
49 self, field.__name__, field, IInputWidget, prefix=self.name
50 )
51+ self.widgets = {
52+ snap_name: getattr(self, "%s_widget" % snap_name)
53+ for snap_name in self.snap_names
54+ }
55 self._widgets_set_up = True
56
57 def setRenderedValue(self, value):
58@@ -60,9 +60,7 @@ class CharmRecipeBuildChannelsWidget(BrowserWidget, InputWidget):
59 if not zope_isinstance(value, dict):
60 value = {}
61 for snap_name in self.snap_names:
62- getattr(self, "%s_widget" % snap_name).setRenderedValue(
63- value.get(snap_name)
64- )
65+ self.widgets[snap_name].setRenderedValue(value.get(snap_name))
66
67 def hasInput(self):
68 """See `IInputWidget`."""
69@@ -86,8 +84,7 @@ class CharmRecipeBuildChannelsWidget(BrowserWidget, InputWidget):
70 self.setUpSubWidgets()
71 channels = {}
72 for snap_name in self.snap_names:
73- widget = getattr(self, snap_name + "_widget")
74- channel = widget.getInputValue()
75+ channel = self.widgets[snap_name].getInputValue()
76 if channel:
77 channels[snap_name] = channel
78 return channels
79diff --git a/lib/lp/app/widgets/templates/snapbuildchannels.pt b/lib/lp/app/widgets/templates/snapbuildchannels.pt
80new file mode 100644
81index 0000000..18de222
82--- /dev/null
83+++ b/lib/lp/app/widgets/templates/snapbuildchannels.pt
84@@ -0,0 +1,12 @@
85+<tal:root
86+ xmlns:tal="http://xml.zope.org/namespaces/tal"
87+ omit-tag="">
88+
89+<table class="subordinate">
90+ <tr tal:repeat="snap_name view/snap_names">
91+ <td tal:content="snap_name" />
92+ <td><div tal:content="structure python: view.widgets[snap_name]()" /></td>
93+ </tr>
94+</table>
95+
96+</tal:root>
97diff --git a/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py b/lib/lp/app/widgets/tests/test_snapbuildchannels.py
98similarity index 72%
99rename from lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py
100rename to lib/lp/app/widgets/tests/test_snapbuildchannels.py
101index ab4a44c..27462a7 100644
102--- a/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py
103+++ b/lib/lp/app/widgets/tests/test_snapbuildchannels.py
104@@ -1,37 +1,33 @@
105-# Copyright 2021 Canonical Ltd. This software is licensed under the
106+# Copyright 2018-2021 Canonical Ltd. This software is licensed under the
107 # GNU Affero General Public License version 3 (see the file LICENSE).
108
109 import re
110
111 from zope.formlib.interfaces import IBrowserWidget, IInputWidget
112-from zope.schema import Dict
113
114-from lp.charms.browser.widgets.charmrecipebuildchannels import (
115- CharmRecipeBuildChannelsWidget,
116-)
117-from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
118+from lp.app.widgets.snapbuildchannels import SnapBuildChannelsWidget
119 from lp.services.beautifulsoup import BeautifulSoup
120-from lp.services.features.testing import FeatureFixture
121+from lp.services.fields import SnapBuildChannelsField
122 from lp.services.webapp.servers import LaunchpadTestRequest
123 from lp.testing import TestCaseWithFactory, verifyObject
124 from lp.testing.layers import DatabaseFunctionalLayer
125
126
127-class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
128+class TestSnapBuildChannelsWidget(TestCaseWithFactory):
129
130 layer = DatabaseFunctionalLayer
131
132 def setUp(self):
133 super().setUp()
134- self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
135- field = Dict(
136+ field = SnapBuildChannelsField(
137 __name__="auto_build_channels",
138 title="Source snap channels for automatic builds",
139+ extra_snap_names=["snapcraft"],
140 )
141- self.context = self.factory.makeCharmRecipe()
142+ self.context = self.factory.makeSnap()
143 self.field = field.bind(self.context)
144 self.request = LaunchpadTestRequest()
145- self.widget = CharmRecipeBuildChannelsWidget(self.field, self.request)
146+ self.widget = SnapBuildChannelsWidget(self.field, self.request)
147
148 def test_implements(self):
149 self.assertTrue(verifyObject(IBrowserWidget, self.widget))
150@@ -39,80 +35,69 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
151
152 def test_template(self):
153 self.assertTrue(
154- self.widget.template.filename.endswith(
155- "charmrecipebuildchannels.pt"
156- ),
157+ self.widget.template.filename.endswith("snapbuildchannels.pt"),
158 "Template was not set up.",
159 )
160
161- def test_hint(self):
162- self.assertEqual(
163- "The channels to use for build tools when building the charm "
164- "recipe.",
165- self.widget.hint,
166- )
167-
168 def test_setUpSubWidgets_first_call(self):
169 # The subwidgets are set up and a flag is set.
170 self.widget.setUpSubWidgets()
171 self.assertTrue(self.widget._widgets_set_up)
172- self.assertIsNotNone(getattr(self.widget, "charmcraft_widget", None))
173 self.assertIsNotNone(getattr(self.widget, "core_widget", None))
174 self.assertIsNotNone(getattr(self.widget, "core18_widget", None))
175 self.assertIsNotNone(getattr(self.widget, "core20_widget", None))
176 self.assertIsNotNone(getattr(self.widget, "core22_widget", None))
177+ self.assertIsNotNone(getattr(self.widget, "snapcraft_widget", None))
178
179 def test_setUpSubWidgets_second_call(self):
180 # The setUpSubWidgets method exits early if a flag is set to
181 # indicate that the widgets were set up.
182 self.widget._widgets_set_up = True
183 self.widget.setUpSubWidgets()
184- self.assertIsNone(getattr(self.widget, "charmcraft_widget", None))
185 self.assertIsNone(getattr(self.widget, "core_widget", None))
186 self.assertIsNone(getattr(self.widget, "core18_widget", None))
187 self.assertIsNone(getattr(self.widget, "core20_widget", None))
188 self.assertIsNone(getattr(self.widget, "core22_widget", None))
189+ self.assertIsNone(getattr(self.widget, "snapcraft_widget", None))
190
191 def test_setRenderedValue_None(self):
192 self.widget.setRenderedValue(None)
193- self.assertIsNone(self.widget.charmcraft_widget._getCurrentValue())
194 self.assertIsNone(self.widget.core_widget._getCurrentValue())
195 self.assertIsNone(self.widget.core18_widget._getCurrentValue())
196 self.assertIsNone(self.widget.core20_widget._getCurrentValue())
197 self.assertIsNone(self.widget.core22_widget._getCurrentValue())
198+ self.assertIsNone(self.widget.snapcraft_widget._getCurrentValue())
199
200 def test_setRenderedValue_empty(self):
201 self.widget.setRenderedValue({})
202- self.assertIsNone(self.widget.charmcraft_widget._getCurrentValue())
203 self.assertIsNone(self.widget.core_widget._getCurrentValue())
204 self.assertIsNone(self.widget.core18_widget._getCurrentValue())
205 self.assertIsNone(self.widget.core20_widget._getCurrentValue())
206 self.assertIsNone(self.widget.core22_widget._getCurrentValue())
207+ self.assertIsNone(self.widget.snapcraft_widget._getCurrentValue())
208
209 def test_setRenderedValue_one_channel(self):
210- self.widget.setRenderedValue({"charmcraft": "stable"})
211- self.assertEqual(
212- "stable", self.widget.charmcraft_widget._getCurrentValue()
213- )
214+ self.widget.setRenderedValue({"snapcraft": "stable"})
215 self.assertIsNone(self.widget.core_widget._getCurrentValue())
216 self.assertIsNone(self.widget.core18_widget._getCurrentValue())
217 self.assertIsNone(self.widget.core20_widget._getCurrentValue())
218+ self.assertIsNone(self.widget.core20_widget._getCurrentValue())
219 self.assertIsNone(self.widget.core22_widget._getCurrentValue())
220+ self.assertEqual(
221+ "stable", self.widget.snapcraft_widget._getCurrentValue()
222+ )
223
224 def test_setRenderedValue_all_channels(self):
225 self.widget.setRenderedValue(
226 {
227- "charmcraft": "stable",
228 "core": "candidate",
229 "core18": "beta",
230 "core20": "edge",
231 "core22": "edge/feature",
232+ "snapcraft": "stable",
233 }
234 )
235 self.assertEqual(
236- "stable", self.widget.charmcraft_widget._getCurrentValue()
237- )
238- self.assertEqual(
239 "candidate", self.widget.core_widget._getCurrentValue()
240 )
241 self.assertEqual("beta", self.widget.core18_widget._getCurrentValue())
242@@ -120,6 +105,9 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
243 self.assertEqual(
244 "edge/feature", self.widget.core22_widget._getCurrentValue()
245 )
246+ self.assertEqual(
247+ "stable", self.widget.snapcraft_widget._getCurrentValue()
248+ )
249
250 def test_hasInput_false(self):
251 # hasInput is false when there are no channels in the form data.
252@@ -129,7 +117,7 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
253 def test_hasInput_true(self):
254 # hasInput is true when there are channels in the form data.
255 self.widget.request = LaunchpadTestRequest(
256- form={"field.auto_build_channels.charmcraft": "stable"}
257+ form={"field.auto_build_channels.snapcraft": "stable"}
258 )
259 self.assertTrue(self.widget.hasInput())
260
261@@ -138,30 +126,30 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
262 # (At the moment, individual channel names are not validated, so
263 # there is no "false" counterpart to this test.)
264 form = {
265- "field.auto_build_channels.charmcraft": "stable",
266 "field.auto_build_channels.core": "",
267 "field.auto_build_channels.core18": "beta",
268 "field.auto_build_channels.core20": "edge",
269 "field.auto_build_channels.core22": "edge/feature",
270+ "field.auto_build_channels.snapcraft": "stable",
271 }
272 self.widget.request = LaunchpadTestRequest(form=form)
273 self.assertTrue(self.widget.hasValidInput())
274
275 def test_getInputValue(self):
276 form = {
277- "field.auto_build_channels.charmcraft": "stable",
278 "field.auto_build_channels.core": "",
279 "field.auto_build_channels.core18": "beta",
280 "field.auto_build_channels.core20": "edge",
281 "field.auto_build_channels.core22": "edge/feature",
282+ "field.auto_build_channels.snapcraft": "stable",
283 }
284 self.widget.request = LaunchpadTestRequest(form=form)
285 self.assertEqual(
286 {
287- "charmcraft": "stable",
288 "core18": "beta",
289 "core20": "edge",
290 "core22": "edge/feature",
291+ "snapcraft": "stable",
292 },
293 self.widget.getInputValue(),
294 )
295@@ -169,19 +157,19 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
296 def test_call(self):
297 # The __call__ method sets up the widgets.
298 markup = self.widget()
299- self.assertIsNotNone(self.widget.charmcraft_widget)
300 self.assertIsNotNone(self.widget.core_widget)
301 self.assertIsNotNone(self.widget.core18_widget)
302 self.assertIsNotNone(self.widget.core20_widget)
303 self.assertIsNotNone(self.widget.core22_widget)
304+ self.assertIsNotNone(self.widget.snapcraft_widget)
305 soup = BeautifulSoup(markup)
306 fields = soup.find_all(["input"], {"id": re.compile(".*")})
307 expected_ids = [
308- "field.auto_build_channels.charmcraft",
309 "field.auto_build_channels.core",
310 "field.auto_build_channels.core18",
311 "field.auto_build_channels.core20",
312 "field.auto_build_channels.core22",
313+ "field.auto_build_channels.snapcraft",
314 ]
315 ids = [field["id"] for field in fields]
316 self.assertContentEqual(expected_ids, ids)
317diff --git a/lib/lp/charms/browser/charmrecipe.py b/lib/lp/charms/browser/charmrecipe.py
318index 9d2dff3..aafa195 100644
319--- a/lib/lp/charms/browser/charmrecipe.py
320+++ b/lib/lp/charms/browser/charmrecipe.py
321@@ -20,6 +20,7 @@ __all__ = [
322 from lazr.restful.interface import copy_field, use_template
323 from zope.component import getUtility
324 from zope.error.interfaces import IErrorReportingUtility
325+from zope.formlib.widget import CustomWidgetFactory
326 from zope.interface import Interface, implementer
327 from zope.schema import TextLine
328 from zope.security.interfaces import Unauthorized
329@@ -32,9 +33,7 @@ from lp.app.browser.launchpadform import (
330 )
331 from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
332 from lp.app.browser.tales import format_link
333-from lp.charms.browser.widgets.charmrecipebuildchannels import (
334- CharmRecipeBuildChannelsWidget,
335-)
336+from lp.app.widgets.snapbuildchannels import SnapBuildChannelsWidget
337 from lp.charms.interfaces.charmhubclient import BadRequestPackageUploadResponse
338 from lp.charms.interfaces.charmrecipe import (
339 CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG,
340@@ -321,7 +320,13 @@ class CharmRecipeAddView(CharmRecipeAuthorizeMixin, LaunchpadFormView):
341 schema = ICharmRecipeEditSchema
342
343 custom_widget_git_ref = GitRefWidget
344- custom_widget_auto_build_channels = CharmRecipeBuildChannelsWidget
345+ custom_widget_auto_build_channels = CustomWidgetFactory(
346+ SnapBuildChannelsWidget,
347+ hint=(
348+ "The channels to use for build tools when building the charm "
349+ "recipe."
350+ ),
351+ )
352 custom_widget_store_channels = StoreChannelsWidget
353
354 next_url = None
355@@ -526,7 +531,13 @@ class CharmRecipeEditView(BaseCharmRecipeEditView):
356 "store_channels",
357 ]
358 custom_widget_git_ref = GitRefWidget
359- custom_widget_auto_build_channels = CharmRecipeBuildChannelsWidget
360+ custom_widget_auto_build_channels = CustomWidgetFactory(
361+ SnapBuildChannelsWidget,
362+ hint=(
363+ "The channels to use for build tools when building the charm "
364+ "recipe."
365+ ),
366+ )
367 custom_widget_store_channels = StoreChannelsWidget
368
369 def validate(self, data):
370@@ -665,7 +676,13 @@ class CharmRecipeRequestBuildsView(LaunchpadFormView):
371 required=True,
372 )
373
374- custom_widget_channels = CharmRecipeBuildChannelsWidget
375+ custom_widget_channels = CustomWidgetFactory(
376+ SnapBuildChannelsWidget,
377+ hint=(
378+ "The channels to use for build tools when building the charm "
379+ "recipe."
380+ ),
381+ )
382
383 @property
384 def cancel_url(self):
385diff --git a/lib/lp/charms/browser/widgets/__init__.py b/lib/lp/charms/browser/widgets/__init__.py
386deleted file mode 100644
387index e69de29..0000000
388--- a/lib/lp/charms/browser/widgets/__init__.py
389+++ /dev/null
390diff --git a/lib/lp/charms/browser/widgets/templates/charmrecipebuildchannels.pt b/lib/lp/charms/browser/widgets/templates/charmrecipebuildchannels.pt
391deleted file mode 100644
392index 3b68f80..0000000
393--- a/lib/lp/charms/browser/widgets/templates/charmrecipebuildchannels.pt
394+++ /dev/null
395@@ -1,28 +0,0 @@
396-<tal:root
397- xmlns:tal="http://xml.zope.org/namespaces/tal"
398- omit-tag="">
399-
400-<table class="subordinate">
401- <tr>
402- <td>charmcraft</td>
403- <td><div tal:content="structure view/charmcraft_widget" /></td>
404- </tr>
405- <tr>
406- <td>core</td>
407- <td><div tal:content="structure view/core_widget" /></td>
408- </tr>
409- <tr>
410- <td>core18</td>
411- <td><div tal:content="structure view/core18_widget" /></td>
412- </tr>
413- <tr>
414- <td>core20</td>
415- <td><div tal:content="structure view/core20_widget" /></td>
416- </tr>
417- <tr>
418- <td>core22</td>
419- <td><div tal:content="structure view/core22_widget" /></td>
420- </tr>
421-</table>
422-
423-</tal:root>
424diff --git a/lib/lp/charms/browser/widgets/tests/__init__.py b/lib/lp/charms/browser/widgets/tests/__init__.py
425deleted file mode 100644
426index e69de29..0000000
427--- a/lib/lp/charms/browser/widgets/tests/__init__.py
428+++ /dev/null
429diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py
430index 6d11a20..3500373 100644
431--- a/lib/lp/snappy/browser/snap.py
432+++ b/lib/lp/snappy/browser/snap.py
433@@ -45,6 +45,7 @@ from lp.app.widgets.itemswidgets import (
434 LaunchpadRadioWidget,
435 LaunchpadRadioWidgetWithDescription,
436 )
437+from lp.app.widgets.snapbuildchannels import SnapBuildChannelsWidget
438 from lp.buildmaster.interfaces.processor import IProcessorSet
439 from lp.code.browser.widgets.gitref import GitRefWidget
440 from lp.code.interfaces.branch import IBranch
441@@ -74,10 +75,10 @@ from lp.services.webapp.interfaces import ICanonicalUrlData
442 from lp.services.webapp.url import urlappend
443 from lp.services.webhooks.browser import WebhookTargetNavigationMixin
444 from lp.snappy.browser.widgets.snaparchive import SnapArchiveWidget
445-from lp.snappy.browser.widgets.snapbuildchannels import SnapBuildChannelsWidget
446 from lp.snappy.browser.widgets.storechannels import StoreChannelsWidget
447 from lp.snappy.interfaces.snap import (
448 SNAP_PRIVATE_FEATURE_FLAG,
449+ SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG,
450 CannotAuthorizeStoreUploads,
451 CannotFetchSnapcraftYaml,
452 CannotParseSnapcraftYaml,
453@@ -396,6 +397,33 @@ def builds_and_requests_for_snap(snap):
454 return items
455
456
457+class HintedSnapBuildChannelsWidget(SnapBuildChannelsWidget):
458+ """A variant of SnapBuildChannelsWidget with appropriate hints."""
459+
460+ def __init__(self, context, request):
461+ super().__init__(context, request)
462+ self.hint = (
463+ "The channels to use for build tools when building the snap "
464+ "package.\n"
465+ )
466+ default_snapcraft_channel = (
467+ getFeatureFlag(SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG) or "apt"
468+ )
469+ if default_snapcraft_channel == "apt":
470+ self.hint += (
471+ 'If unset, or if the channel for snapcraft is set to "apt", '
472+ "the default is to install snapcraft from the source archive "
473+ "using apt."
474+ )
475+ else:
476+ self.hint += (
477+ 'If unset, the default is to install snapcraft from the "%s" '
478+ 'channel. Setting the channel for snapcraft to "apt" causes '
479+ "snapcraft to be installed from the source archive using "
480+ "apt." % default_snapcraft_channel
481+ )
482+
483+
484 class SnapRequestBuildsView(LaunchpadFormView):
485 """A view for requesting builds of a snap package."""
486
487@@ -434,7 +462,7 @@ class SnapRequestBuildsView(LaunchpadFormView):
488 custom_widget_archive = SnapArchiveWidget
489 custom_widget_distro_arch_series = LabeledMultiCheckBoxWidget
490 custom_widget_pocket = LaunchpadDropdownWidget
491- custom_widget_channels = SnapBuildChannelsWidget
492+ custom_widget_channels = HintedSnapBuildChannelsWidget
493
494 help_links = {
495 "pocket": "/+help-snappy/snap-build-pocket.html",
496@@ -565,7 +593,7 @@ class SnapAddView(
497 custom_widget_store_distro_series = LaunchpadRadioWidget
498 custom_widget_auto_build_archive = SnapArchiveWidget
499 custom_widget_auto_build_pocket = LaunchpadDropdownWidget
500- custom_widget_auto_build_channels = SnapBuildChannelsWidget
501+ custom_widget_auto_build_channels = HintedSnapBuildChannelsWidget
502 custom_widget_store_channels = StoreChannelsWidget
503
504 help_links = {
505@@ -973,7 +1001,7 @@ class SnapEditView(BaseSnapEditView, EnableProcessorsMixin):
506 )
507 custom_widget_auto_build_archive = SnapArchiveWidget
508 custom_widget_auto_build_pocket = LaunchpadDropdownWidget
509- custom_widget_auto_build_channels = SnapBuildChannelsWidget
510+ custom_widget_auto_build_channels = HintedSnapBuildChannelsWidget
511 custom_widget_store_channels = StoreChannelsWidget
512 # See `setUpWidgets` method.
513 custom_widget_information_type = CustomWidgetFactory(
514diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
515index c6e6e0c..5e9cfac 100644
516--- a/lib/lp/snappy/browser/tests/test_snap.py
517+++ b/lib/lp/snappy/browser/tests/test_snap.py
518@@ -31,6 +31,9 @@ from zope.testbrowser.browser import LinkNotFoundError
519
520 from lp.app.enums import InformationType
521 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
522+from lp.app.widgets.tests.test_snapbuildchannels import (
523+ TestSnapBuildChannelsWidget,
524+)
525 from lp.buildmaster.enums import BuildStatus
526 from lp.buildmaster.interfaces.processor import IProcessorSet
527 from lp.code.errors import BranchHostingFault, GitRepositoryScanFault
528@@ -50,9 +53,15 @@ from lp.services.job.interfaces.job import JobStatus
529 from lp.services.propertycache import get_property_cache
530 from lp.services.webapp import canonical_url
531 from lp.services.webapp.servers import LaunchpadTestRequest
532-from lp.snappy.browser.snap import SnapAdminView, SnapEditView, SnapView
533+from lp.snappy.browser.snap import (
534+ HintedSnapBuildChannelsWidget,
535+ SnapAdminView,
536+ SnapEditView,
537+ SnapView,
538+)
539 from lp.snappy.interfaces.snap import (
540 SNAP_PRIVATE_FEATURE_FLAG,
541+ SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG,
542 SNAP_TESTING_FLAGS,
543 CannotModifySnapProcessor,
544 ISnapSet,
545@@ -2545,6 +2554,51 @@ class TestSnapView(BaseTestSnapView):
546 self.assertEqual(authorize_url, authorize_link.url)
547
548
549+class TestHintedSnapBuildChannelsWidget(TestSnapBuildChannelsWidget):
550+ def setUp(self):
551+ super().setUp()
552+ self.widget = HintedSnapBuildChannelsWidget(self.field, self.request)
553+
554+ def test_hint_no_feature_flag(self):
555+ self.assertEqual(
556+ "The channels to use for build tools when building the snap "
557+ "package.\n"
558+ 'If unset, or if the channel for snapcraft is set to "apt", '
559+ "the default is to install snapcraft from the source archive "
560+ "using apt.",
561+ self.widget.hint,
562+ )
563+
564+ def test_hint_feature_flag_apt(self):
565+ self.useFixture(
566+ FeatureFixture({SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG: "apt"})
567+ )
568+ widget = HintedSnapBuildChannelsWidget(self.field, self.request)
569+ self.assertEqual(
570+ "The channels to use for build tools when building the snap "
571+ "package.\n"
572+ 'If unset, or if the channel for snapcraft is set to "apt", '
573+ "the default is to install snapcraft from the source archive "
574+ "using apt.",
575+ widget.hint,
576+ )
577+
578+ def test_hint_feature_flag_real_channel(self):
579+ self.useFixture(
580+ FeatureFixture({SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG: "stable"})
581+ )
582+ widget = HintedSnapBuildChannelsWidget(self.field, self.request)
583+ self.assertEqual(
584+ "The channels to use for build tools when building the snap "
585+ "package.\n"
586+ 'If unset, the default is to install snapcraft from the "stable" '
587+ 'channel. Setting the channel for snapcraft to "apt" causes '
588+ "snapcraft to be installed from the source archive using "
589+ "apt.",
590+ widget.hint,
591+ )
592+
593+
594 class TestSnapRequestBuildsView(BaseTestSnapView):
595 def setUp(self):
596 super().setUp()
597@@ -2603,6 +2657,7 @@ class TestSnapRequestBuildsView(BaseTestSnapView):
598 core20
599 core22
600 snapcraft
601+ snapd
602 The channels to use for build tools when building the snap
603 package.
604 If unset, or if the channel for snapcraft is set to "apt", the
605diff --git a/lib/lp/snappy/browser/widgets/snapbuildchannels.py b/lib/lp/snappy/browser/widgets/snapbuildchannels.py
606deleted file mode 100644
607index 17a6786..0000000
608--- a/lib/lp/snappy/browser/widgets/snapbuildchannels.py
609+++ /dev/null
610@@ -1,125 +0,0 @@
611-# Copyright 2018-2019 Canonical Ltd. This software is licensed under the
612-# GNU Affero General Public License version 3 (see the file LICENSE).
613-
614-"""A widget for selecting source snap channels for builds."""
615-
616-__all__ = [
617- "SnapBuildChannelsWidget",
618-]
619-
620-from zope.browserpage import ViewPageTemplateFile
621-from zope.formlib.interfaces import IInputWidget
622-from zope.formlib.utility import setUpWidget
623-from zope.formlib.widget import BrowserWidget, InputErrors, InputWidget
624-from zope.interface import implementer
625-from zope.schema import TextLine
626-from zope.security.proxy import isinstance as zope_isinstance
627-
628-from lp.app.errors import UnexpectedFormData
629-from lp.services.features import getFeatureFlag
630-from lp.services.webapp.interfaces import (
631- IAlwaysSubmittedWidget,
632- ISingleLineWidgetLayout,
633-)
634-from lp.snappy.interfaces.snap import SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG
635-
636-
637-@implementer(ISingleLineWidgetLayout, IAlwaysSubmittedWidget, IInputWidget)
638-class SnapBuildChannelsWidget(BrowserWidget, InputWidget):
639-
640- template = ViewPageTemplateFile("templates/snapbuildchannels.pt")
641- hint = False
642- snap_names = ["core", "core18", "core20", "core22", "snapcraft"]
643- _widgets_set_up = False
644-
645- def __init__(self, context, request):
646- super().__init__(context, request)
647- self.hint = (
648- "The channels to use for build tools when building the snap "
649- "package.\n"
650- )
651- default_snapcraft_channel = (
652- getFeatureFlag(SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG) or "apt"
653- )
654- if default_snapcraft_channel == "apt":
655- self.hint += (
656- 'If unset, or if the channel for snapcraft is set to "apt", '
657- "the default is to install snapcraft from the source archive "
658- "using apt."
659- )
660- else:
661- self.hint += (
662- 'If unset, the default is to install snapcraft from the "%s" '
663- 'channel. Setting the channel for snapcraft to "apt" causes '
664- "snapcraft to be installed from the source archive using "
665- "apt." % default_snapcraft_channel
666- )
667-
668- def setUpSubWidgets(self):
669- if self._widgets_set_up:
670- return
671- fields = [
672- TextLine(
673- __name__=snap_name,
674- title="%s channel" % snap_name,
675- required=False,
676- )
677- for snap_name in self.snap_names
678- ]
679- for field in fields:
680- setUpWidget(
681- self, field.__name__, field, IInputWidget, prefix=self.name
682- )
683- self._widgets_set_up = True
684-
685- def setRenderedValue(self, value):
686- """See `IWidget`."""
687- self.setUpSubWidgets()
688- if not zope_isinstance(value, dict):
689- value = {}
690- for snap_name in self.snap_names:
691- getattr(self, "%s_widget" % snap_name).setRenderedValue(
692- value.get(snap_name)
693- )
694-
695- def hasInput(self):
696- """See `IInputWidget`."""
697- return any(
698- "%s.%s" % (self.name, snap_name) in self.request.form
699- for snap_name in self.snap_names
700- )
701-
702- def hasValidInput(self):
703- """See `IInputWidget`."""
704- try:
705- self.getInputValue()
706- return True
707- except InputErrors:
708- return False
709- except UnexpectedFormData:
710- return False
711-
712- def getInputValue(self):
713- """See `IInputWidget`."""
714- self.setUpSubWidgets()
715- channels = {}
716- for snap_name in self.snap_names:
717- widget = getattr(self, snap_name + "_widget")
718- channel = widget.getInputValue()
719- if channel:
720- channels[snap_name] = channel
721- return channels
722-
723- def error(self):
724- """See `IBrowserWidget`."""
725- try:
726- if self.hasInput():
727- self.getInputValue()
728- except InputErrors as error:
729- self._error = error
730- return super().error()
731-
732- def __call__(self):
733- """See `IBrowserWidget`."""
734- self.setUpSubWidgets()
735- return self.template()
736diff --git a/lib/lp/snappy/browser/widgets/templates/snapbuildchannels.pt b/lib/lp/snappy/browser/widgets/templates/snapbuildchannels.pt
737deleted file mode 100644
738index 5a92fdf..0000000
739--- a/lib/lp/snappy/browser/widgets/templates/snapbuildchannels.pt
740+++ /dev/null
741@@ -1,28 +0,0 @@
742-<tal:root
743- xmlns:tal="http://xml.zope.org/namespaces/tal"
744- omit-tag="">
745-
746-<table class="subordinate">
747- <tr>
748- <td>core</td>
749- <td><div tal:content="structure view/core_widget" /></td>
750- </tr>
751- <tr>
752- <td>core18</td>
753- <td><div tal:content="structure view/core18_widget" /></td>
754- </tr>
755- <tr>
756- <td>core20</td>
757- <td><div tal:content="structure view/core20_widget" /></td>
758- </tr>
759- <tr>
760- <td>core22</td>
761- <td><div tal:content="structure view/core22_widget" /></td>
762- </tr>
763- <tr>
764- <td>snapcraft</td>
765- <td><div tal:content="structure view/snapcraft_widget" /></td>
766- </tr>
767-</table>
768-
769-</tal:root>
770diff --git a/lib/lp/snappy/browser/widgets/tests/test_snapbuildchannelswidget.py b/lib/lp/snappy/browser/widgets/tests/test_snapbuildchannelswidget.py
771deleted file mode 100644
772index 9b71b4c..0000000
773--- a/lib/lp/snappy/browser/widgets/tests/test_snapbuildchannelswidget.py
774+++ /dev/null
775@@ -1,215 +0,0 @@
776-# Copyright 2018-2019 Canonical Ltd. This software is licensed under the
777-# GNU Affero General Public License version 3 (see the file LICENSE).
778-
779-import re
780-
781-from zope.formlib.interfaces import IBrowserWidget, IInputWidget
782-from zope.schema import Dict
783-
784-from lp.services.beautifulsoup import BeautifulSoup
785-from lp.services.features.testing import FeatureFixture
786-from lp.services.webapp.servers import LaunchpadTestRequest
787-from lp.snappy.browser.widgets.snapbuildchannels import SnapBuildChannelsWidget
788-from lp.snappy.interfaces.snap import SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG
789-from lp.testing import TestCaseWithFactory, verifyObject
790-from lp.testing.layers import DatabaseFunctionalLayer
791-
792-
793-class TestSnapBuildChannelsWidget(TestCaseWithFactory):
794-
795- layer = DatabaseFunctionalLayer
796-
797- def setUp(self):
798- super().setUp()
799- field = Dict(
800- __name__="auto_build_channels",
801- title="Source snap channels for automatic builds",
802- )
803- self.context = self.factory.makeSnap()
804- self.field = field.bind(self.context)
805- self.request = LaunchpadTestRequest()
806- self.widget = SnapBuildChannelsWidget(self.field, self.request)
807-
808- def test_implements(self):
809- self.assertTrue(verifyObject(IBrowserWidget, self.widget))
810- self.assertTrue(verifyObject(IInputWidget, self.widget))
811-
812- def test_template(self):
813- self.assertTrue(
814- self.widget.template.filename.endswith("snapbuildchannels.pt"),
815- "Template was not set up.",
816- )
817-
818- def test_hint_no_feature_flag(self):
819- self.assertEqual(
820- "The channels to use for build tools when building the snap "
821- "package.\n"
822- 'If unset, or if the channel for snapcraft is set to "apt", '
823- "the default is to install snapcraft from the source archive "
824- "using apt.",
825- self.widget.hint,
826- )
827-
828- def test_hint_feature_flag_apt(self):
829- self.useFixture(
830- FeatureFixture({SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG: "apt"})
831- )
832- widget = SnapBuildChannelsWidget(self.field, self.request)
833- self.assertEqual(
834- "The channels to use for build tools when building the snap "
835- "package.\n"
836- 'If unset, or if the channel for snapcraft is set to "apt", '
837- "the default is to install snapcraft from the source archive "
838- "using apt.",
839- widget.hint,
840- )
841-
842- def test_hint_feature_flag_real_channel(self):
843- self.useFixture(
844- FeatureFixture({SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG: "stable"})
845- )
846- widget = SnapBuildChannelsWidget(self.field, self.request)
847- self.assertEqual(
848- "The channels to use for build tools when building the snap "
849- "package.\n"
850- 'If unset, the default is to install snapcraft from the "stable" '
851- 'channel. Setting the channel for snapcraft to "apt" causes '
852- "snapcraft to be installed from the source archive using "
853- "apt.",
854- widget.hint,
855- )
856-
857- def test_setUpSubWidgets_first_call(self):
858- # The subwidgets are set up and a flag is set.
859- self.widget.setUpSubWidgets()
860- self.assertTrue(self.widget._widgets_set_up)
861- self.assertIsNotNone(getattr(self.widget, "core_widget", None))
862- self.assertIsNotNone(getattr(self.widget, "core18_widget", None))
863- self.assertIsNotNone(getattr(self.widget, "core20_widget", None))
864- self.assertIsNotNone(getattr(self.widget, "core22_widget", None))
865- self.assertIsNotNone(getattr(self.widget, "snapcraft_widget", None))
866-
867- def test_setUpSubWidgets_second_call(self):
868- # The setUpSubWidgets method exits early if a flag is set to
869- # indicate that the widgets were set up.
870- self.widget._widgets_set_up = True
871- self.widget.setUpSubWidgets()
872- self.assertIsNone(getattr(self.widget, "core_widget", None))
873- self.assertIsNone(getattr(self.widget, "core18_widget", None))
874- self.assertIsNone(getattr(self.widget, "core20_widget", None))
875- self.assertIsNone(getattr(self.widget, "core22_widget", None))
876- self.assertIsNone(getattr(self.widget, "snapcraft_widget", None))
877-
878- def test_setRenderedValue_None(self):
879- self.widget.setRenderedValue(None)
880- self.assertIsNone(self.widget.core_widget._getCurrentValue())
881- self.assertIsNone(self.widget.core18_widget._getCurrentValue())
882- self.assertIsNone(self.widget.core20_widget._getCurrentValue())
883- self.assertIsNone(self.widget.core22_widget._getCurrentValue())
884- self.assertIsNone(self.widget.snapcraft_widget._getCurrentValue())
885-
886- def test_setRenderedValue_empty(self):
887- self.widget.setRenderedValue({})
888- self.assertIsNone(self.widget.core_widget._getCurrentValue())
889- self.assertIsNone(self.widget.core18_widget._getCurrentValue())
890- self.assertIsNone(self.widget.core20_widget._getCurrentValue())
891- self.assertIsNone(self.widget.core22_widget._getCurrentValue())
892- self.assertIsNone(self.widget.snapcraft_widget._getCurrentValue())
893-
894- def test_setRenderedValue_one_channel(self):
895- self.widget.setRenderedValue({"snapcraft": "stable"})
896- self.assertIsNone(self.widget.core_widget._getCurrentValue())
897- self.assertIsNone(self.widget.core18_widget._getCurrentValue())
898- self.assertIsNone(self.widget.core20_widget._getCurrentValue())
899- self.assertIsNone(self.widget.core20_widget._getCurrentValue())
900- self.assertIsNone(self.widget.core22_widget._getCurrentValue())
901- self.assertEqual(
902- "stable", self.widget.snapcraft_widget._getCurrentValue()
903- )
904-
905- def test_setRenderedValue_all_channels(self):
906- self.widget.setRenderedValue(
907- {
908- "core": "candidate",
909- "core18": "beta",
910- "core20": "edge",
911- "core22": "edge/feature",
912- "snapcraft": "stable",
913- }
914- )
915- self.assertEqual(
916- "candidate", self.widget.core_widget._getCurrentValue()
917- )
918- self.assertEqual("beta", self.widget.core18_widget._getCurrentValue())
919- self.assertEqual("edge", self.widget.core20_widget._getCurrentValue())
920- self.assertEqual(
921- "edge/feature", self.widget.core22_widget._getCurrentValue()
922- )
923- self.assertEqual(
924- "stable", self.widget.snapcraft_widget._getCurrentValue()
925- )
926-
927- def test_hasInput_false(self):
928- # hasInput is false when there are no channels in the form data.
929- self.widget.request = LaunchpadTestRequest(form={})
930- self.assertFalse(self.widget.hasInput())
931-
932- def test_hasInput_true(self):
933- # hasInput is true when there are channels in the form data.
934- self.widget.request = LaunchpadTestRequest(
935- form={"field.auto_build_channels.snapcraft": "stable"}
936- )
937- self.assertTrue(self.widget.hasInput())
938-
939- def test_hasValidInput_true(self):
940- # The field input is valid when all submitted channels are valid.
941- # (At the moment, individual channel names are not validated, so
942- # there is no "false" counterpart to this test.)
943- form = {
944- "field.auto_build_channels.core": "",
945- "field.auto_build_channels.core18": "beta",
946- "field.auto_build_channels.core20": "edge",
947- "field.auto_build_channels.core22": "edge/feature",
948- "field.auto_build_channels.snapcraft": "stable",
949- }
950- self.widget.request = LaunchpadTestRequest(form=form)
951- self.assertTrue(self.widget.hasValidInput())
952-
953- def test_getInputValue(self):
954- form = {
955- "field.auto_build_channels.core": "",
956- "field.auto_build_channels.core18": "beta",
957- "field.auto_build_channels.core20": "edge",
958- "field.auto_build_channels.core22": "edge/feature",
959- "field.auto_build_channels.snapcraft": "stable",
960- }
961- self.widget.request = LaunchpadTestRequest(form=form)
962- self.assertEqual(
963- {
964- "core18": "beta",
965- "core20": "edge",
966- "core22": "edge/feature",
967- "snapcraft": "stable",
968- },
969- self.widget.getInputValue(),
970- )
971-
972- def test_call(self):
973- # The __call__ method sets up the widgets.
974- markup = self.widget()
975- self.assertIsNotNone(self.widget.core_widget)
976- self.assertIsNotNone(self.widget.core18_widget)
977- self.assertIsNotNone(self.widget.core20_widget)
978- self.assertIsNotNone(self.widget.core22_widget)
979- self.assertIsNotNone(self.widget.snapcraft_widget)
980- soup = BeautifulSoup(markup)
981- fields = soup.find_all(["input"], {"id": re.compile(".*")})
982- expected_ids = [
983- "field.auto_build_channels.core",
984- "field.auto_build_channels.core18",
985- "field.auto_build_channels.core20",
986- "field.auto_build_channels.core22",
987- "field.auto_build_channels.snapcraft",
988- ]
989- ids = [field["id"] for field in fields]
990- self.assertContentEqual(expected_ids, ids)

Subscribers

People subscribed via source and target branches

to status/vote changes: