Merge ~pappacena/launchpad:snap-create-on-project into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 403e170be00e91616d12420701102bf034ea7953
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:snap-create-on-project
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:snap-pillar-edit
Diff against target: 379 lines (+176/-46)
4 files modified
lib/lp/snappy/browser/configure.zcml (+6/-0)
lib/lp/snappy/browser/snap.py (+87/-43)
lib/lp/snappy/browser/tests/test_snap.py (+43/-0)
lib/lp/snappy/templates/snap-new.pt (+40/-3)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+399184@code.launchpad.net

Commit message

Adding +new-snap page for project

Description of the change

The link on the project overview to this new page is proposed in another MP (https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/399224) to keep this MP shorter for review.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

Looks generally OK, but I'd like to know what we're planning to do about the store name extraction issue, and I'd also like to see a screenshot or two of the new UI.

review: Needs Information
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes and replied about the store name issue.

Screenshots:
- Create snap from gitref: https://private-fileshare.canonical.com/~pappacena/screenshots/private-oci-recipe/new-snap-gitref.png

- Create snap from project: https://private-fileshare.canonical.com/~pappacena/screenshots/private-oci-recipe/new-snap-project.png

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes.

Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/snappy/browser/configure.zcml b/lib/lp/snappy/browser/configure.zcml
2index ebbbb66..a21c0b4 100644
3--- a/lib/lp/snappy/browser/configure.zcml
4+++ b/lib/lp/snappy/browser/configure.zcml
5@@ -87,6 +87,12 @@
6 name="+new-snap"
7 template="../templates/snap-new.pt" />
8 <browser:page
9+ for="lp.registry.interfaces.product.IProduct"
10+ class="lp.snappy.browser.snap.SnapAddView"
11+ permission="launchpad.AnyPerson"
12+ name="+new-snap"
13+ template="../templates/snap-new.pt" />
14+ <browser:page
15 for="lp.snappy.interfaces.snap.ISnap"
16 class="lp.snappy.browser.snap.SnapRequestBuildsView"
17 permission="launchpad.Edit"
18diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py
19index 0d76973..701dc19 100644
20--- a/lib/lp/snappy/browser/snap.py
21+++ b/lib/lp/snappy/browser/snap.py
22@@ -61,11 +61,13 @@ from lp.app.widgets.itemswidgets import (
23 )
24 from lp.buildmaster.interfaces.processor import IProcessorSet
25 from lp.code.browser.widgets.gitref import GitRefWidget
26+from lp.code.interfaces.branch import IBranch
27 from lp.code.interfaces.gitref import IGitRef
28 from lp.registry.enums import VCSType
29 from lp.registry.interfaces.person import IPersonSet
30 from lp.registry.interfaces.personproduct import IPersonProductFactory
31 from lp.registry.interfaces.pocket import PackagePublishingPocket
32+from lp.registry.interfaces.product import IProduct
33 from lp.services.features import getFeatureFlag
34 from lp.services.propertycache import cachedproperty
35 from lp.services.scripts import log
36@@ -164,6 +166,29 @@ class SnapNavigation(WebhookTargetNavigationMixin, Navigation):
37 return self.context.getSubscription(person)
38
39
40+class SnapFormMixin:
41+ def validateVCSWidgets(self, cls, data):
42+ """Validates if VCS sub-widgets."""
43+ # Set widgets as required or optional depending on the vcs field.
44+ vcs = data.get('vcs')
45+ if vcs == VCSType.BZR:
46+ self.widgets['branch'].context.required = True
47+ self.widgets['git_ref'].context.required = False
48+ elif vcs == VCSType.GIT:
49+ self.widgets['branch'].context.required = False
50+ self.widgets['git_ref'].context.required = True
51+ else:
52+ raise AssertionError("Unknown branch type %s" % vcs)
53+
54+ def setUpVCSWidgets(self):
55+ widget = self.widgets.get('vcs')
56+ if widget is not None:
57+ current_value = widget._getFormValue()
58+ self.vcs_bzr_radio, self.vcs_git_radio = [
59+ render_radio_widget_part(widget, value, current_value)
60+ for value in (VCSType.BZR, VCSType.GIT)]
61+
62+
63 class SnapInformationTypeMixin:
64 def getPossibleInformationTypes(self, snap, user):
65 """Get the information types to display on the edit form.
66@@ -183,7 +208,10 @@ class SnapInformationTypeMixin:
67 information types will be calculated based on the project.
68 """
69 info_type = data.get('information_type')
70- project = data.get('project')
71+ if IProduct.providedBy(self.context):
72+ project = self.context
73+ else:
74+ project = data.get('project')
75 if info_type is None and project is None:
76 # Nothing to validate here. Move on.
77 return
78@@ -497,28 +525,17 @@ class SnapAuthorizeMixin:
79 log_oops(e, self.request)
80
81
82-class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin, EnableProcessorsMixin,
83- SnapInformationTypeMixin):
84+class SnapAddView(SnapAuthorizeMixin, EnableProcessorsMixin,
85+ SnapInformationTypeMixin, SnapFormMixin, LaunchpadFormView):
86 """View for creating snap packages."""
87
88 page_title = label = 'Create a new snap package'
89
90 schema = ISnapEditSchema
91- field_names = [
92- 'owner',
93- 'name',
94- 'project',
95- 'information_type',
96- 'store_distro_series',
97- 'build_source_tarball',
98- 'auto_build',
99- 'auto_build_archive',
100- 'auto_build_pocket',
101- 'auto_build_channels',
102- 'store_upload',
103- 'store_name',
104- 'store_channels',
105- ]
106+
107+ custom_widget_vcs = LaunchpadRadioWidget
108+ custom_widget_git_ref = CustomWidgetFactory(
109+ GitRefWidget, allow_external=True)
110 custom_widget_store_distro_series = LaunchpadRadioWidget
111 custom_widget_auto_build_archive = SnapArchiveWidget
112 custom_widget_auto_build_pocket = LaunchpadDropdownWidget
113@@ -529,6 +546,26 @@ class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin, EnableProcessorsMixin,
114 "auto_build_pocket": "/+help-snappy/snap-build-pocket.html",
115 }
116
117+ @property
118+ def field_names(self):
119+ fields = ['owner', 'name']
120+ if self.is_project_context:
121+ fields += ['vcs', 'branch', 'git_ref']
122+ else:
123+ fields += ['project']
124+ return fields + [
125+ 'information_type',
126+ 'store_distro_series',
127+ 'build_source_tarball',
128+ 'auto_build',
129+ 'auto_build_archive',
130+ 'auto_build_pocket',
131+ 'auto_build_channels',
132+ 'store_upload',
133+ 'store_name',
134+ 'store_channels',
135+ ]
136+
137 def initialize(self):
138 """See `LaunchpadView`."""
139 super(SnapAddView, self).initialize()
140@@ -540,6 +577,10 @@ class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin, EnableProcessorsMixin,
141 self.context.information_type in PRIVATE_INFORMATION_TYPES):
142 raise SnapPrivateFeatureDisabled
143
144+ @property
145+ def is_project_context(self):
146+ return IProduct.providedBy(self.context)
147+
148 def setUpFields(self):
149 """See `LaunchpadFormView`."""
150 super(SnapAddView, self).setUpFields()
151@@ -553,6 +594,15 @@ class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin, EnableProcessorsMixin,
152 """See `LaunchpadFormView`."""
153 super(SnapAddView, self).setUpWidgets()
154 self.widgets['processors'].widget_class = 'processors'
155+ if self.is_project_context:
156+ # If we are on Project:+new-snap page, we know which information
157+ # types the project supports. Let's filter out the ones that are
158+ # not supported.
159+ types = getUtility(ISnapSet).getPossibleSnapInformationTypes(
160+ self.context)
161+ info_type_widget = self.widgets['information_type']
162+ info_type_widget.vocabulary = InformationTypeVocabulary(types)
163+ self.setUpVCSWidgets()
164
165 @property
166 def cancel_url(self):
167@@ -561,7 +611,7 @@ class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin, EnableProcessorsMixin,
168 @property
169 def initial_values(self):
170 store_name = None
171- if self.has_snappy_distro_series:
172+ if self.has_snappy_distro_series and not self.is_project_context:
173 # Try to extract Snap store name from snapcraft.yaml file.
174 try:
175 snapcraft_data = getUtility(ISnapSet).getSnapcraftYaml(
176@@ -606,6 +656,9 @@ class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin, EnableProcessorsMixin,
177
178 def validate_widgets(self, data, names=None):
179 """See `LaunchpadFormView`."""
180+ if self.widgets.get('vcs') is not None:
181+ super(SnapAddView, self).validate_widgets(data, ['vcs'])
182+ self.validateVCSWidgets(SnapAddView, data)
183 if self.widgets.get('auto_build') is not None:
184 # Set widgets as required or optional depending on the
185 # auto_build field.
186@@ -625,9 +678,17 @@ class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin, EnableProcessorsMixin,
187 @action('Create snap package', name='create')
188 def create_action(self, action, data):
189 if IGitRef.providedBy(self.context):
190- kwargs = {'git_ref': self.context}
191+ kwargs = {'git_ref': self.context, 'project': data['project']}
192+ elif IBranch.providedBy(self.context):
193+ kwargs = {'branch': self.context, 'project': data['project']}
194+ elif self.is_project_context:
195+ if data['vcs'] == VCSType.GIT:
196+ kwargs = {'git_ref': data['git_ref']}
197+ else:
198+ kwargs = {'branch': data['branch']}
199+ kwargs['project'] = self.context
200 else:
201- kwargs = {'branch': self.context}
202+ raise NotImplementedError("Unknown context for snap creation.")
203 if not data.get('auto_build', False):
204 data['auto_build_archive'] = None
205 data['auto_build_pocket'] = None
206@@ -639,7 +700,6 @@ class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin, EnableProcessorsMixin,
207 auto_build_pocket=data['auto_build_pocket'],
208 auto_build_channels=data['auto_build_channels'],
209 information_type=data['information_type'],
210- project=data['project'],
211 processors=data['processors'],
212 build_source_tarball=data['build_source_tarball'],
213 store_upload=data['store_upload'],
214@@ -664,8 +724,8 @@ class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin, EnableProcessorsMixin,
215 self.validateInformationType(data)
216
217
218-class BaseSnapEditView(LaunchpadEditFormView, SnapAuthorizeMixin,
219- SnapInformationTypeMixin):
220+class BaseSnapEditView(SnapAuthorizeMixin, SnapInformationTypeMixin,
221+ SnapFormMixin, LaunchpadEditFormView):
222
223 schema = ISnapEditSchema
224
225@@ -675,13 +735,8 @@ class BaseSnapEditView(LaunchpadEditFormView, SnapAuthorizeMixin,
226
227 def setUpWidgets(self, context=None):
228 """See `LaunchpadFormView`."""
229- super(BaseSnapEditView, self).setUpWidgets(context=None)
230- widget = self.widgets.get('vcs')
231- if widget is not None:
232- current_value = widget._getFormValue()
233- self.vcs_bzr_radio, self.vcs_git_radio = [
234- render_radio_widget_part(widget, value, current_value)
235- for value in (VCSType.BZR, VCSType.GIT)]
236+ super(BaseSnapEditView, self).setUpWidgets()
237+ self.setUpVCSWidgets()
238
239 @property
240 def has_snappy_distro_series(self):
241@@ -690,18 +745,8 @@ class BaseSnapEditView(LaunchpadEditFormView, SnapAuthorizeMixin,
242 def validate_widgets(self, data, names=None):
243 """See `LaunchpadFormView`."""
244 if self.widgets.get('vcs') is not None:
245- # Set widgets as required or optional depending on the vcs
246- # field.
247 super(BaseSnapEditView, self).validate_widgets(data, ['vcs'])
248- vcs = data.get('vcs')
249- if vcs == VCSType.BZR:
250- self.widgets['branch'].context.required = True
251- self.widgets['git_ref'].context.required = False
252- elif vcs == VCSType.GIT:
253- self.widgets['branch'].context.required = False
254- self.widgets['git_ref'].context.required = True
255- else:
256- raise AssertionError("Unknown branch type %s" % vcs)
257+ self.validateVCSWidgets(BaseSnapEditView, data)
258 if self.widgets.get('auto_build') is not None:
259 # Set widgets as required or optional depending on the
260 # auto_build field.
261@@ -747,7 +792,6 @@ class BaseSnapEditView(LaunchpadEditFormView, SnapAuthorizeMixin,
262 'A public snap cannot have a private repository.')
263 self.validateInformationType(data, snap=self.context)
264
265-
266 def _needStoreReauth(self, data):
267 """Does this change require reauthorizing to the store?"""
268 store_upload = data.get('store_upload', False)
269diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
270index 894326b..9852eb0 100644
271--- a/lib/lp/snappy/browser/tests/test_snap.py
272+++ b/lib/lp/snappy/browser/tests/test_snap.py
273@@ -339,6 +339,49 @@ class TestSnapAddView(BaseTestSnapView):
274 "the store.\nEdit snap package",
275 MatchesTagText(content, "store_upload"))
276
277+ def test_create_new_snap_project(self):
278+ self.useFixture(GitHostingFixture(blob=b""))
279+ project = self.factory.makeProduct()
280+ [git_ref] = self.factory.makeGitRefs()
281+ source_display = git_ref.display_name
282+ browser = self.getViewBrowser(
283+ project, view_name="+new-snap", user=self.person)
284+ browser.getControl(name="field.name").value = "snap-name"
285+ browser.getControl(name="field.vcs").value = "GIT"
286+ browser.getControl(name="field.git_ref.repository").value = (
287+ git_ref.repository.shortened_path)
288+ browser.getControl(name="field.git_ref.path").value = git_ref.path
289+ browser.getControl("Create snap package").click()
290+
291+ content = find_main_content(browser.contents)
292+ self.assertEqual("snap-name", extract_text(content.h1))
293+ self.assertThat(
294+ "Test Person", MatchesPickerText(content, "edit-owner"))
295+ self.assertThat(
296+ "Distribution series:\n%s\nEdit snap package" %
297+ self.distroseries.fullseriesname,
298+ MatchesTagText(content, "distro_series"))
299+ self.assertThat(
300+ "Source:\n%s\nEdit snap package" % source_display,
301+ MatchesTagText(content, "source"))
302+ self.assertThat(
303+ "Build source tarball:\nNo\nEdit snap package",
304+ MatchesTagText(content, "build_source_tarball"))
305+ self.assertThat(
306+ "Build schedule:\n(?)\nBuilt on request\nEdit snap package\n",
307+ MatchesTagText(content, "auto_build"))
308+ self.assertThat(
309+ "Source archive for automatic builds:\n\nEdit snap package\n",
310+ MatchesTagText(content, "auto_build_archive"))
311+ self.assertThat(
312+ "Pocket for automatic builds:\n\nEdit snap package",
313+ MatchesTagText(content, "auto_build_pocket"))
314+ self.assertIsNone(find_tag_by_id(content, "auto_build_channels"))
315+ self.assertThat(
316+ "Builds of this snap package are not automatically uploaded to "
317+ "the store.\nEdit snap package",
318+ MatchesTagText(content, "store_upload"))
319+
320 def test_create_new_snap_users_teams_as_owner_options(self):
321 # Teams that the user is in are options for the snap package owner.
322 self.useFixture(BranchHostingFixture(blob=b""))
323diff --git a/lib/lp/snappy/templates/snap-new.pt b/lib/lp/snappy/templates/snap-new.pt
324index 5334bee..480d51c 100644
325--- a/lib/lp/snappy/templates/snap-new.pt
326+++ b/lib/lp/snappy/templates/snap-new.pt
327@@ -30,12 +30,49 @@
328 <tal:widget define="widget nocall:view/widgets/owner">
329 <metal:block use-macro="context/@@launchpad_form/widget_row" />
330 </tal:widget>
331- <tal:widget define="widget nocall:view/widgets/project">
332- <metal:block use-macro="context/@@launchpad_form/widget_row" />
333- </tal:widget>
334+
335+ <tal:guard condition="not: view/is_project_context">
336+ <tal:widget define="widget nocall:view/widgets/project">
337+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
338+ </tal:widget>
339+ </tal:guard>
340 <tal:widget define="widget nocall:view/widgets/information_type">
341 <metal:block use-macro="context/@@launchpad_form/widget_row" />
342 </tal:widget>
343+
344+ <tr tal:condition="view/is_project_context">
345+ <td>
346+ <div>
347+ <label for="field.vcs">Source:</label>
348+ <table>
349+ <tr>
350+ <td>
351+ <label tal:replace="structure view/vcs_bzr_radio" />
352+ <table class="subordinate">
353+ <tal:widget define="widget nocall:view/widgets/branch">
354+ <metal:block
355+ use-macro="context/@@launchpad_form/widget_row" />
356+ </tal:widget>
357+ </table>
358+ </td>
359+ </tr>
360+ <tr>
361+ <td>
362+ <label tal:replace="structure view/vcs_git_radio" />
363+ <table class="subordinate">
364+ <tal:widget define="widget
365+ nocall:view/widgets/git_ref">
366+ <metal:block
367+ use-macro="context/@@launchpad_form/widget_row" />
368+ </tal:widget>
369+ </table>
370+ </td>
371+ </tr>
372+ </table>
373+ </div>
374+ </td>
375+ </tr>
376+
377 <tal:widget define="widget nocall:view/widgets/store_distro_series">
378 <metal:block use-macro="context/@@launchpad_form/widget_row" />
379 </tal:widget>