Merge lp:~cjwatson/launchpad/snap-add-view into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17747
Proposed branch: lp:~cjwatson/launchpad/snap-add-view
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/snap-listings
Diff against target: 432 lines (+251/-15)
9 files modified
lib/lp/code/browser/branch.py (+4/-4)
lib/lp/code/browser/gitref.py (+6/-3)
lib/lp/snappy/browser/configure.zcml (+12/-0)
lib/lp/snappy/browser/hassnaps.py (+8/-0)
lib/lp/snappy/browser/snap.py (+64/-7)
lib/lp/snappy/browser/tests/test_snap.py (+110/-1)
lib/lp/snappy/templates/snap-macros.pt (+6/-0)
lib/lp/snappy/templates/snap-new.pt (+40/-0)
lib/lp/snappy/tests/test_snapbuildbehaviour.py (+1/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-add-view
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+271360@code.launchpad.net

Commit message

Add views for creating snap packages based on Bazaar or Git branches.

Description of the change

Add views for creating snap packages based on Bazaar or Git branches.

GitRepository:+index now has a "View snap packages" link but no way to create one directly; you have to go through a ref, which might be slightly non-obvious. I did this because it simplified SnapAddView to not have to worry about the source at all for now and just take it all from the context. This could be improved in future if need be.

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
1=== modified file 'lib/lp/code/browser/branch.py'
2--- lib/lp/code/browser/branch.py 2015-09-16 13:30:33 +0000
3+++ lib/lp/code/browser/branch.py 2015-09-18 11:06:25 +0000
4@@ -277,10 +277,10 @@
5 usedfor = IBranch
6 facet = 'branches'
7 links = [
8- 'add_subscriber', 'browse_revisions', 'create_recipe', 'link_bug',
9- 'link_blueprint', 'register_merge', 'source', 'subscription',
10- 'edit_status', 'edit_import', 'upgrade_branch', 'view_recipes',
11- 'view_snaps', 'visibility']
12+ 'add_subscriber', 'browse_revisions', 'create_recipe', 'create_snap',
13+ 'link_bug', 'link_blueprint', 'register_merge', 'source',
14+ 'subscription', 'edit_status', 'edit_import', 'upgrade_branch',
15+ 'view_recipes', 'view_snaps', 'visibility']
16
17 @enabled_with_permission('launchpad.Edit')
18 def edit_status(self):
19
20=== modified file 'lib/lp/code/browser/gitref.py'
21--- lib/lp/code/browser/gitref.py 2015-09-16 13:30:33 +0000
22+++ lib/lp/code/browser/gitref.py 2015-09-18 11:06:25 +0000
23@@ -54,7 +54,10 @@
24 stepthrough,
25 )
26 from lp.services.webapp.authorization import check_permission
27-from lp.snappy.browser.hassnaps import HasSnapsViewMixin
28+from lp.snappy.browser.hassnaps import (
29+ HasSnapsMenuMixin,
30+ HasSnapsViewMixin,
31+ )
32
33
34 # XXX cjwatson 2015-05-26: We can get rid of this after a short while, since
35@@ -76,12 +79,12 @@
36 return self.redirectSubTree(canonical_url(proposal))
37
38
39-class GitRefContextMenu(ContextMenu):
40+class GitRefContextMenu(ContextMenu, HasSnapsMenuMixin):
41 """Context menu for Git references."""
42
43 usedfor = IGitRef
44 facet = 'branches'
45- links = ['register_merge']
46+ links = ['create_snap', 'register_merge']
47
48 def register_merge(self):
49 text = 'Propose for merging'
50
51=== modified file 'lib/lp/snappy/browser/configure.zcml'
52--- lib/lp/snappy/browser/configure.zcml 2015-09-17 12:31:53 +0000
53+++ lib/lp/snappy/browser/configure.zcml 2015-09-18 11:06:25 +0000
54@@ -29,6 +29,18 @@
55 module="lp.snappy.browser.snap"
56 classes="SnapNavigation" />
57 <browser:page
58+ for="lp.code.interfaces.branch.IBranch"
59+ class="lp.snappy.browser.snap.SnapAddView"
60+ permission="launchpad.AnyPerson"
61+ name="+new-snap"
62+ template="../templates/snap-new.pt" />
63+ <browser:page
64+ for="lp.code.interfaces.gitref.IGitRef"
65+ class="lp.snappy.browser.snap.SnapAddView"
66+ permission="launchpad.AnyPerson"
67+ name="+new-snap"
68+ template="../templates/snap-new.pt" />
69+ <browser:page
70 for="lp.snappy.interfaces.snap.ISnap"
71 class="lp.snappy.browser.snap.SnapAdminView"
72 permission="launchpad.Admin"
73
74=== modified file 'lib/lp/snappy/browser/hassnaps.py'
75--- lib/lp/snappy/browser/hassnaps.py 2015-09-17 10:37:37 +0000
76+++ lib/lp/snappy/browser/hassnaps.py 2015-09-18 11:06:25 +0000
77@@ -33,6 +33,14 @@
78 context, visible_by_user=self.user).is_empty()
79 return Link('+snaps', text, icon='info', enabled=enabled)
80
81+ def create_snap(self):
82+ # You can't yet create a snap for a private branch.
83+ enabled = (
84+ bool(getFeatureFlag(SNAP_FEATURE_FLAG)) and
85+ not self.context.private)
86+ text = 'Create snap package'
87+ return Link('+new-snap', text, enabled=enabled, icon='add')
88+
89
90 class HasSnapsViewMixin:
91 """A view mixin for objects that implement IHasSnaps."""
92
93=== modified file 'lib/lp/snappy/browser/snap.py'
94--- lib/lp/snappy/browser/snap.py 2015-09-16 13:30:33 +0000
95+++ lib/lp/snappy/browser/snap.py 2015-09-18 11:06:25 +0000
96@@ -5,6 +5,7 @@
97
98 __metaclass__ = type
99 __all__ = [
100+ 'SnapAddView',
101 'SnapDeleteView',
102 'SnapEditView',
103 'SnapNavigation',
104@@ -24,13 +25,17 @@
105 action,
106 custom_widget,
107 LaunchpadEditFormView,
108+ LaunchpadFormView,
109 render_radio_widget_part,
110 )
111 from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
112 from lp.app.browser.tales import format_link
113+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
114 from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
115 from lp.code.browser.widgets.gitref import GitRefWidget
116+from lp.code.interfaces.gitref import IGitRef
117 from lp.registry.enums import VCSType
118+from lp.services.features import getFeatureFlag
119 from lp.services.webapp import (
120 canonical_url,
121 enabled_with_permission,
122@@ -48,6 +53,8 @@
123 from lp.snappy.interfaces.snap import (
124 ISnap,
125 ISnapSet,
126+ SNAP_FEATURE_FLAG,
127+ SnapFeatureDisabled,
128 NoSuchSnap,
129 )
130 from lp.snappy.interfaces.snapbuild import ISnapBuildSet
131@@ -156,7 +163,60 @@
132 git_ref = copy_field(ISnap['git_ref'], required=True)
133
134
135-class BaseSnapAddEditView(LaunchpadEditFormView):
136+class SnapAddView(LaunchpadFormView):
137+ """View for creating snap packages."""
138+
139+ page_title = label = 'Create a new snap package'
140+
141+ schema = ISnapEditSchema
142+ field_names = ['owner', 'name', 'distro_series']
143+ custom_widget('distro_series', LaunchpadRadioWidget)
144+
145+ def initialize(self):
146+ """See `LaunchpadView`."""
147+ if not getFeatureFlag(SNAP_FEATURE_FLAG):
148+ raise SnapFeatureDisabled
149+ super(SnapAddView, self).initialize()
150+
151+ @property
152+ def cancel_url(self):
153+ return canonical_url(self.context)
154+
155+ @property
156+ def initial_values(self):
157+ # XXX cjwatson 2015-09-18: Hack to ensure that we don't end up
158+ # accidentally selecting ubuntu-rtm/14.09 or similar.
159+ # ubuntu.currentseries will always be in BuildableDistroSeries.
160+ series = getUtility(ILaunchpadCelebrities).ubuntu.currentseries
161+ return {
162+ 'owner': self.user,
163+ 'distro_series': series,
164+ }
165+
166+ @action('Create snap package', name='create')
167+ def request_action(self, action, data):
168+ if IGitRef.providedBy(self.context):
169+ kwargs = {'git_ref': self.context}
170+ else:
171+ kwargs = {'branch': self.context}
172+ snap = getUtility(ISnapSet).new(
173+ self.user, data['owner'], data['distro_series'], data['name'],
174+ **kwargs)
175+ self.next_url = canonical_url(snap)
176+
177+ def validate(self, data):
178+ super(SnapAddView, self).validate(data)
179+ owner = data.get('owner', None)
180+ name = data.get('name', None)
181+ if owner and name:
182+ if getUtility(ISnapSet).exists(owner, name):
183+ self.setFieldError(
184+ 'name',
185+ 'There is already a snap package owned by %s with this '
186+ 'name.' % owner.displayname)
187+
188+
189+class BaseSnapEditView(LaunchpadEditFormView):
190
191 schema = ISnapEditSchema
192
193@@ -166,7 +226,7 @@
194
195 def setUpWidgets(self):
196 """See `LaunchpadFormView`."""
197- super(BaseSnapAddEditView, self).setUpWidgets()
198+ super(BaseSnapEditView, self).setUpWidgets()
199 widget = self.widgets.get('vcs')
200 if widget is not None:
201 current_value = widget._getFormValue()
202@@ -179,7 +239,7 @@
203 if 'vcs' in self.widgets:
204 # Set widgets as required or optional depending on the vcs
205 # field.
206- super(BaseSnapAddEditView, self).validate_widgets(data, ['vcs'])
207+ super(BaseSnapEditView, self).validate_widgets(data, ['vcs'])
208 vcs = data.get('vcs')
209 if vcs == VCSType.BZR:
210 self.widgets['branch'].context.required = True
211@@ -189,10 +249,7 @@
212 self.widgets['git_ref'].context.required = True
213 else:
214 raise AssertionError("Unknown branch type %s" % vcs)
215- super(BaseSnapAddEditView, self).validate_widgets(data, names=names)
216-
217-
218-class BaseSnapEditView(BaseSnapAddEditView):
219+ super(BaseSnapEditView, self).validate_widgets(data, names=names)
220
221 @action('Update snap package', name='update')
222 def request_action(self, action, data):
223
224=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
225--- lib/lp/snappy/browser/tests/test_snap.py 2015-09-10 20:01:25 +0000
226+++ lib/lp/snappy/browser/tests/test_snap.py 2015-09-18 11:06:25 +0000
227@@ -30,7 +30,10 @@
228 SnapEditView,
229 SnapView,
230 )
231-from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
232+from lp.snappy.interfaces.snap import (
233+ SNAP_FEATURE_FLAG,
234+ SnapFeatureDisabled,
235+ )
236 from lp.testing import (
237 BrowserTestCase,
238 login,
239@@ -53,6 +56,7 @@
240 find_tags_by_class,
241 )
242 from lp.testing.publication import test_traverse
243+from lp.testing.views import create_initialized_view
244
245
246 class TestSnapNavigation(TestCaseWithFactory):
247@@ -77,6 +81,111 @@
248 self.assertEqual(snap, obj)
249
250
251+class TestSnapViewsFeatureFlag(TestCaseWithFactory):
252+
253+ layer = DatabaseFunctionalLayer
254+
255+ def test_feature_flag_disabled(self):
256+ # Without a feature flag, we will not create new Snaps.
257+ branch = self.factory.makeAnyBranch()
258+ self.assertRaises(
259+ SnapFeatureDisabled, create_initialized_view, branch, "+new-snap")
260+
261+
262+class TestSnapAddView(BrowserTestCase):
263+
264+ layer = DatabaseFunctionalLayer
265+
266+ def setUp(self):
267+ super(TestSnapAddView, self).setUp()
268+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
269+ self.useFixture(FakeLogger())
270+ self.person = self.factory.makePerson(
271+ name="test-person", displayname="Test Person")
272+
273+ def test_initial_distroseries(self):
274+ # The initial distroseries is the newest that is current or in
275+ # development.
276+ archive = self.factory.makeArchive(owner=self.person)
277+ self.factory.makeDistroSeries(
278+ distribution=archive.distribution, version="14.04",
279+ status=SeriesStatus.DEVELOPMENT)
280+ development = self.factory.makeDistroSeries(
281+ distribution=archive.distribution, version="14.10",
282+ status=SeriesStatus.DEVELOPMENT)
283+ self.factory.makeDistroSeries(
284+ distribution=archive.distribution, version="15.04",
285+ status=SeriesStatus.EXPERIMENTAL)
286+ branch = self.factory.makeAnyBranch()
287+ with person_logged_in(self.person):
288+ view = create_initialized_view(branch, "+new-snap")
289+ self.assertEqual(development, view.initial_values["distro_series"])
290+
291+ def test_create_new_snap_not_logged_in(self):
292+ branch = self.factory.makeAnyBranch()
293+ self.assertRaises(
294+ Unauthorized, self.getViewBrowser, branch, view_name="+new-snap",
295+ no_login=True)
296+
297+ def test_create_new_snap_bzr(self):
298+ archive = self.factory.makeArchive()
299+ distroseries = self.factory.makeDistroSeries(
300+ distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)
301+ branch = self.factory.makeAnyBranch()
302+ source_display = branch.display_name
303+ browser = self.getViewBrowser(
304+ branch, view_name="+new-snap", user=self.person)
305+ browser.getControl("Name").value = "snap-name"
306+ browser.getControl("Create snap package").click()
307+
308+ content = find_main_content(browser.contents)
309+ self.assertEqual("snap-name", extract_text(content.h1))
310+ self.assertThat(
311+ "Test Person", MatchesPickerText(content, "edit-owner"))
312+ self.assertThat(
313+ "Distribution series:\n%s\nEdit snap package" %
314+ distroseries.fullseriesname,
315+ MatchesTagText(content, "distro_series"))
316+ self.assertThat(
317+ "Source:\n%s\nEdit snap package" % source_display,
318+ MatchesTagText(content, "source"))
319+
320+ def test_create_new_snap_git(self):
321+ archive = self.factory.makeArchive()
322+ distroseries = self.factory.makeDistroSeries(
323+ distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)
324+ [git_ref] = self.factory.makeGitRefs()
325+ source_display = git_ref.display_name
326+ browser = self.getViewBrowser(
327+ git_ref, view_name="+new-snap", user=self.person)
328+ browser.getControl("Name").value = "snap-name"
329+ browser.getControl("Create snap package").click()
330+
331+ content = find_main_content(browser.contents)
332+ self.assertEqual("snap-name", extract_text(content.h1))
333+ self.assertThat(
334+ "Test Person", MatchesPickerText(content, "edit-owner"))
335+ self.assertThat(
336+ "Distribution series:\n%s\nEdit snap package" %
337+ distroseries.fullseriesname,
338+ MatchesTagText(content, "distro_series"))
339+ self.assertThat(
340+ "Source:\n%s\nEdit snap package" % source_display,
341+ MatchesTagText(content, "source"))
342+
343+ def test_create_new_snap_users_teams_as_owner_options(self):
344+ # Teams that the user is in are options for the snap package owner.
345+ self.factory.makeTeam(
346+ name="test-team", displayname="Test Team", members=[self.person])
347+ branch = self.factory.makeAnyBranch()
348+ browser = self.getViewBrowser(
349+ branch, view_name="+new-snap", user=self.person)
350+ options = browser.getControl("Owner").displayOptions
351+ self.assertEqual(
352+ ["Test Person (test-person)", "Test Team (test-team)"],
353+ sorted(str(option) for option in options))
354+
355+
356 class TestSnapAdminView(BrowserTestCase):
357
358 layer = DatabaseFunctionalLayer
359
360=== modified file 'lib/lp/snappy/templates/snap-macros.pt'
361--- lib/lp/snappy/templates/snap-macros.pt 2015-09-17 12:23:18 +0000
362+++ lib/lp/snappy/templates/snap-macros.pt 2015-09-18 11:06:25 +0000
363@@ -24,6 +24,12 @@
364 </div>
365 </div>
366
367+ <span
368+ tal:define="link context_menu/create_snap|nothing"
369+ tal:condition="python: link and link.enabled"
370+ tal:replace="structure link/render"
371+ />
372+
373 </div>
374
375 </tal:root>
376
377=== added file 'lib/lp/snappy/templates/snap-new.pt'
378--- lib/lp/snappy/templates/snap-new.pt 1970-01-01 00:00:00 +0000
379+++ lib/lp/snappy/templates/snap-new.pt 2015-09-18 11:06:25 +0000
380@@ -0,0 +1,40 @@
381+<html
382+ xmlns="http://www.w3.org/1999/xhtml"
383+ xmlns:tal="http://xml.zope.org/namespaces/tal"
384+ xmlns:metal="http://xml.zope.org/namespaces/metal"
385+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
386+ metal:use-macro="view/macro:page/main_side"
387+ i18n:domain="launchpad">
388+<body>
389+
390+<div metal:fill-slot="main">
391+ <div>
392+ <p>
393+ A snap package is a self-contained application that can be installed
394+ on <a href="https://developer.ubuntu.com/en/snappy/">snappy Ubuntu
395+ Core</a>. Launchpad can build snap packages using <a
396+ href="https://developer.ubuntu.com/en/snappy/snapcraft/">snapcraft</a>,
397+ from any Git or Bazaar branch on Launchpad that has a
398+ <tt>snapcraft.yaml</tt> file at its top level.
399+ </p>
400+ </div>
401+
402+ <div metal:use-macro="context/@@launchpad_form/form">
403+ <metal:formbody fill-slot="widgets">
404+ <table class="form">
405+ <tal:widget define="widget nocall:view/widgets/name">
406+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
407+ </tal:widget>
408+ <tal:widget define="widget nocall:view/widgets/owner">
409+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
410+ </tal:widget>
411+ <tal:widget define="widget nocall:view/widgets/distro_series">
412+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
413+ </tal:widget>
414+ </table>
415+ </metal:formbody>
416+ </div>
417+</div>
418+
419+</body>
420+</html>
421
422=== modified file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py'
423--- lib/lp/snappy/tests/test_snapbuildbehaviour.py 2015-09-09 14:17:46 +0000
424+++ lib/lp/snappy/tests/test_snapbuildbehaviour.py 2015-09-18 11:06:25 +0000
425@@ -49,6 +49,7 @@
426 def setUp(self):
427 super(TestSnapBuildBehaviour, self).setUp()
428 self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
429+ self.pushConfig("snappy", tools_source=None)
430
431 def makeJob(self, pocket=PackagePublishingPocket.RELEASE, **kwargs):
432 """Create a sample `ISnapBuildBehaviour`."""