Merge lp:~cjwatson/launchpad/snap-store-add-edit-views into lp:launchpad
- snap-store-add-edit-views
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18073 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snap-store-add-edit-views | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/snap-authorize-view | ||||
Diff against target: |
865 lines (+428/-56) 12 files modified
lib/lp/app/browser/configure.zcml (+6/-0) lib/lp/app/browser/tales.py (+9/-0) lib/lp/snappy/browser/snap.py (+123/-19) lib/lp/snappy/browser/tests/test_snap.py (+184/-34) lib/lp/snappy/interfaces/snap.py (+9/-1) lib/lp/snappy/interfaces/snappyseries.py (+3/-0) lib/lp/snappy/model/snap.py (+13/-0) lib/lp/snappy/model/snappyseries.py (+4/-0) lib/lp/snappy/templates/snap-edit.pt (+19/-1) lib/lp/snappy/templates/snap-index.pt (+22/-0) lib/lp/snappy/templates/snap-new.pt (+19/-1) lib/lp/snappy/tests/test_snappyseries.py (+17/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-store-add-edit-views | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+294524@code.launchpad.net |
Commit message
Allow configuring automatic store upload in SnapAddView and SnapEditView.
Description of the change
Allow configuring automatic store upload in SnapAddView and SnapEditView.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Revision history for this message
Colin Watson (cjwatson) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/app/browser/configure.zcml' |
2 | --- lib/lp/app/browser/configure.zcml 2015-08-07 10:12:38 +0000 |
3 | +++ lib/lp/app/browser/configure.zcml 2016-05-27 10:39:59 +0000 |
4 | @@ -853,6 +853,12 @@ |
5 | name="fmt" |
6 | /> |
7 | <adapter |
8 | + for="lp.snappy.interfaces.snappyseries.ISnappySeries" |
9 | + provides="zope.traversing.interfaces.IPathAdapter" |
10 | + factory="lp.app.browser.tales.SnappySeriesFormatterAPI" |
11 | + name="fmt" |
12 | + /> |
13 | + <adapter |
14 | for="lp.blueprints.interfaces.specification.ISpecification" |
15 | provides="zope.traversing.interfaces.IPathAdapter" |
16 | factory="lp.app.browser.tales.SpecificationFormatterAPI" |
17 | |
18 | === modified file 'lib/lp/app/browser/tales.py' |
19 | --- lib/lp/app/browser/tales.py 2016-02-04 04:39:30 +0000 |
20 | +++ lib/lp/app/browser/tales.py 2016-05-27 10:39:59 +0000 |
21 | @@ -1872,6 +1872,15 @@ |
22 | 'owner': self._context.owner.displayname} |
23 | |
24 | |
25 | +class SnappySeriesFormatterAPI(CustomizableFormatter): |
26 | + """Adapter providing fmt support for ISnappySeries objects.""" |
27 | + |
28 | + _link_summary_template = _('%(title)s') |
29 | + |
30 | + def _link_summary_values(self): |
31 | + return {'title': self._context.title} |
32 | + |
33 | + |
34 | class SpecificationFormatterAPI(CustomizableFormatter): |
35 | """Adapter providing fmt support for Specification objects""" |
36 | |
37 | |
38 | === modified file 'lib/lp/snappy/browser/snap.py' |
39 | --- lib/lp/snappy/browser/snap.py 2016-05-24 04:45:38 +0000 |
40 | +++ lib/lp/snappy/browser/snap.py 2016-05-27 10:39:59 +0000 |
41 | @@ -26,6 +26,7 @@ |
42 | ) |
43 | from pymacaroons import Macaroon |
44 | from zope.component import getUtility |
45 | +from zope.error.interfaces import IErrorReportingUtility |
46 | from zope.interface import Interface |
47 | from zope.schema import ( |
48 | Choice, |
49 | @@ -87,7 +88,11 @@ |
50 | SnapPrivateFeatureDisabled, |
51 | ) |
52 | from lp.snappy.interfaces.snapbuild import ISnapBuildSet |
53 | -from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient |
54 | +from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet |
55 | +from lp.snappy.interfaces.snapstoreclient import ( |
56 | + BadRequestPackageUploadResponse, |
57 | + ISnapStoreClient, |
58 | + ) |
59 | from lp.soyuz.browser.archive import EnableProcessorsMixin |
60 | from lp.soyuz.browser.build import get_build_by_id_str |
61 | from lp.soyuz.interfaces.archive import IArchive |
62 | @@ -296,9 +301,11 @@ |
63 | 'name', |
64 | 'private', |
65 | 'require_virtualized', |
66 | + 'store_upload', |
67 | ]) |
68 | - distro_series = Choice( |
69 | - vocabulary='BuildableDistroSeries', title=u'Distribution series') |
70 | + store_distro_series = Choice( |
71 | + vocabulary='BuildableSnappyDistroSeries', required=True, |
72 | + title=u'Series') |
73 | vcs = Choice(vocabulary=VCSType, required=True, title=u'VCS') |
74 | |
75 | # Each of these is only required if vcs has an appropriate value. Later |
76 | @@ -306,15 +313,44 @@ |
77 | branch = copy_field(ISnap['branch'], required=True) |
78 | git_ref = copy_field(ISnap['git_ref'], required=True) |
79 | |
80 | - |
81 | -class SnapAddView(LaunchpadFormView): |
82 | + # These are only required if store_upload is True. Later validation |
83 | + # takes care of adjusting the required attribute. |
84 | + store_name = copy_field(ISnap['store_name'], required=True) |
85 | + |
86 | + |
87 | +def log_oops(error, request): |
88 | + """Log an oops report without raising an error.""" |
89 | + info = (error.__class__, error, None) |
90 | + getUtility(IErrorReportingUtility).raising(info, request) |
91 | + |
92 | + |
93 | +class SnapAuthorizeMixin: |
94 | + |
95 | + def requestAuthorization(self, snap): |
96 | + try: |
97 | + self.next_url = SnapAuthorizeView.requestAuthorization( |
98 | + snap, self.request) |
99 | + except BadRequestPackageUploadResponse as e: |
100 | + self.setFieldError( |
101 | + 'store_upload', |
102 | + 'Cannot get permission from the store to upload this package.') |
103 | + log_oops(e, self.request) |
104 | + |
105 | + |
106 | +class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin): |
107 | """View for creating snap packages.""" |
108 | |
109 | page_title = label = 'Create a new snap package' |
110 | |
111 | schema = ISnapEditSchema |
112 | - field_names = ['owner', 'name', 'distro_series'] |
113 | - custom_widget('distro_series', LaunchpadRadioWidget) |
114 | + field_names = [ |
115 | + 'owner', |
116 | + 'name', |
117 | + 'store_distro_series', |
118 | + 'store_upload', |
119 | + 'store_name', |
120 | + ] |
121 | + custom_widget('store_distro_series', LaunchpadRadioWidget) |
122 | |
123 | def initialize(self): |
124 | """See `LaunchpadView`.""" |
125 | @@ -340,13 +376,28 @@ |
126 | # accidentally selecting ubuntu-rtm/14.09 or similar. |
127 | # ubuntu.currentseries will always be in BuildableDistroSeries. |
128 | series = getUtility(ILaunchpadCelebrities).ubuntu.currentseries |
129 | + sds_set = getUtility(ISnappyDistroSeriesSet) |
130 | return { |
131 | 'owner': self.user, |
132 | - 'distro_series': series, |
133 | + 'store_distro_series': sds_set.getByDistroSeries(series).first(), |
134 | } |
135 | |
136 | + @property |
137 | + def has_snappy_distro_series(self): |
138 | + return not getUtility(ISnappyDistroSeriesSet).getAll().is_empty() |
139 | + |
140 | + def validate_widgets(self, data, names=None): |
141 | + """See `LaunchpadFormView`.""" |
142 | + if self.widgets.get('store_upload') is not None: |
143 | + # Set widgets as required or optional depending on the |
144 | + # store_upload field. |
145 | + super(SnapAddView, self).validate_widgets(data, ['store_upload']) |
146 | + store_upload = data.get('store_upload', False) |
147 | + self.widgets['store_name'].context.required = store_upload |
148 | + super(SnapAddView, self).validate_widgets(data, names=names) |
149 | + |
150 | @action('Create snap package', name='create') |
151 | - def request_action(self, action, data): |
152 | + def create_action(self, action, data): |
153 | if IGitRef.providedBy(self.context): |
154 | kwargs = {'git_ref': self.context} |
155 | else: |
156 | @@ -354,9 +405,15 @@ |
157 | private = not getUtility( |
158 | ISnapSet).isValidPrivacy(False, data['owner'], **kwargs) |
159 | snap = getUtility(ISnapSet).new( |
160 | - self.user, data['owner'], data['distro_series'], data['name'], |
161 | - private=private, **kwargs) |
162 | - self.next_url = canonical_url(snap) |
163 | + self.user, data['owner'], |
164 | + data['store_distro_series'].distro_series, data['name'], |
165 | + private=private, store_upload=data['store_upload'], |
166 | + store_series=data['store_distro_series'].snappy_series, |
167 | + store_name=data['store_name'], **kwargs) |
168 | + if data['store_upload']: |
169 | + self.requestAuthorization(snap) |
170 | + else: |
171 | + self.next_url = canonical_url(snap) |
172 | |
173 | def validate(self, data): |
174 | super(SnapAddView, self).validate(data) |
175 | @@ -370,7 +427,7 @@ |
176 | 'name.' % owner.displayname) |
177 | |
178 | |
179 | -class BaseSnapEditView(LaunchpadEditFormView): |
180 | +class BaseSnapEditView(LaunchpadEditFormView, SnapAuthorizeMixin): |
181 | |
182 | schema = ISnapEditSchema |
183 | |
184 | @@ -388,6 +445,10 @@ |
185 | render_radio_widget_part(widget, value, current_value) |
186 | for value in (VCSType.BZR, VCSType.GIT)] |
187 | |
188 | + @property |
189 | + def has_snappy_distro_series(self): |
190 | + return not getUtility(ISnappyDistroSeriesSet).getAll().is_empty() |
191 | + |
192 | def validate_widgets(self, data, names=None): |
193 | """See `LaunchpadFormView`.""" |
194 | if self.widgets.get('vcs') is not None: |
195 | @@ -403,8 +464,29 @@ |
196 | self.widgets['git_ref'].context.required = True |
197 | else: |
198 | raise AssertionError("Unknown branch type %s" % vcs) |
199 | + if self.widgets.get('store_upload') is not None: |
200 | + # Set widgets as required or optional depending on the |
201 | + # store_upload field. |
202 | + super(BaseSnapEditView, self).validate_widgets( |
203 | + data, ['store_upload']) |
204 | + store_upload = data.get('store_upload', False) |
205 | + self.widgets['store_name'].context.required = store_upload |
206 | super(BaseSnapEditView, self).validate_widgets(data, names=names) |
207 | |
208 | + def _needStoreReauth(self, data): |
209 | + """Does this change require reauthorizing to the store?""" |
210 | + store_upload = data.get('store_upload', False) |
211 | + store_distro_series = data.get('store_distro_series') |
212 | + store_name = data.get('store_name') |
213 | + if (not store_upload or |
214 | + store_distro_series is None or store_name is None): |
215 | + return False |
216 | + if store_distro_series.snappy_series != self.context.store_series: |
217 | + return True |
218 | + if store_name != self.context.store_name: |
219 | + return True |
220 | + return False |
221 | + |
222 | @action('Update snap package', name='update') |
223 | def request_action(self, action, data): |
224 | vcs = data.pop('vcs', None) |
225 | @@ -418,8 +500,15 @@ |
226 | self.context.setProcessors( |
227 | new_processors, check_permissions=True, user=self.user) |
228 | del data['processors'] |
229 | + store_upload = data.get('store_upload', False) |
230 | + if not store_upload: |
231 | + data['store_name'] = None |
232 | + need_store_reauth = self._needStoreReauth(data) |
233 | self.updateContextFromData(data) |
234 | - self.next_url = canonical_url(self.context) |
235 | + if need_store_reauth: |
236 | + self.requestAuthorization(self.context) |
237 | + else: |
238 | + self.next_url = canonical_url(self.context) |
239 | |
240 | @property |
241 | def adapters(self): |
242 | @@ -462,8 +551,16 @@ |
243 | page_title = 'Edit' |
244 | |
245 | field_names = [ |
246 | - 'owner', 'name', 'distro_series', 'vcs', 'branch', 'git_ref'] |
247 | - custom_widget('distro_series', LaunchpadRadioWidget) |
248 | + 'owner', |
249 | + 'name', |
250 | + 'store_distro_series', |
251 | + 'store_upload', |
252 | + 'store_name', |
253 | + 'vcs', |
254 | + 'branch', |
255 | + 'git_ref', |
256 | + ] |
257 | + custom_widget('store_distro_series', LaunchpadRadioWidget) |
258 | custom_widget('vcs', LaunchpadRadioWidget) |
259 | custom_widget('git_ref', GitRefWidget) |
260 | |
261 | @@ -478,11 +575,18 @@ |
262 | |
263 | @property |
264 | def initial_values(self): |
265 | + initial_values = {} |
266 | + if self.context.store_series is None: |
267 | + # XXX cjwatson 2016-04-26: Remove this case once all existing |
268 | + # Snaps have had a store_series backfilled. |
269 | + sds_set = getUtility(ISnappyDistroSeriesSet) |
270 | + initial_values['store_distro_series'] = sds_set.getByDistroSeries( |
271 | + self.context.distro_series).first() |
272 | if self.context.git_ref is not None: |
273 | - vcs = VCSType.GIT |
274 | + initial_values['vcs'] = VCSType.GIT |
275 | else: |
276 | - vcs = VCSType.BZR |
277 | - return {'vcs': vcs} |
278 | + initial_values['vcs'] = VCSType.BZR |
279 | + return initial_values |
280 | |
281 | def validate(self, data): |
282 | super(SnapEditView, self).validate(data) |
283 | |
284 | === modified file 'lib/lp/snappy/browser/tests/test_snap.py' |
285 | --- lib/lp/snappy/browser/tests/test_snap.py 2016-05-24 04:45:38 +0000 |
286 | +++ lib/lp/snappy/browser/tests/test_snap.py 2016-05-27 10:39:59 +0000 |
287 | @@ -53,6 +53,7 @@ |
288 | ) |
289 | from lp.snappy.interfaces.snap import ( |
290 | CannotModifySnapProcessor, |
291 | + ISnapSet, |
292 | SNAP_FEATURE_FLAG, |
293 | SNAP_TESTING_FLAGS, |
294 | SnapFeatureDisabled, |
295 | @@ -139,7 +140,7 @@ |
296 | |
297 | class TestSnapAddView(BrowserTestCase): |
298 | |
299 | - layer = DatabaseFunctionalLayer |
300 | + layer = LaunchpadFunctionalLayer |
301 | |
302 | def setUp(self): |
303 | super(TestSnapAddView, self).setUp() |
304 | @@ -147,24 +148,35 @@ |
305 | self.useFixture(FakeLogger()) |
306 | self.person = self.factory.makePerson( |
307 | name="test-person", displayname="Test Person") |
308 | + self.distroseries = self.factory.makeUbuntuDistroSeries( |
309 | + version="13.10") |
310 | + with admin_logged_in(): |
311 | + self.snappyseries = self.factory.makeSnappySeries( |
312 | + usable_distro_series=[self.distroseries]) |
313 | |
314 | def test_initial_distroseries(self): |
315 | # The initial distroseries is the newest that is current or in |
316 | # development. |
317 | - archive = self.factory.makeArchive(owner=self.person) |
318 | - self.factory.makeDistroSeries( |
319 | - distribution=archive.distribution, version="14.04", |
320 | - status=SeriesStatus.DEVELOPMENT) |
321 | - development = self.factory.makeDistroSeries( |
322 | - distribution=archive.distribution, version="14.10", |
323 | - status=SeriesStatus.DEVELOPMENT) |
324 | - self.factory.makeDistroSeries( |
325 | - distribution=archive.distribution, version="15.04", |
326 | - status=SeriesStatus.EXPERIMENTAL) |
327 | + old = self.factory.makeUbuntuDistroSeries( |
328 | + version="14.04", status=SeriesStatus.DEVELOPMENT) |
329 | + development = self.factory.makeUbuntuDistroSeries( |
330 | + version="14.10", status=SeriesStatus.DEVELOPMENT) |
331 | + experimental = self.factory.makeUbuntuDistroSeries( |
332 | + version="15.04", status=SeriesStatus.EXPERIMENTAL) |
333 | + with admin_logged_in(): |
334 | + self.factory.makeSnappySeries( |
335 | + usable_distro_series=[old, development, experimental]) |
336 | + newest = self.factory.makeSnappySeries( |
337 | + usable_distro_series=[development, experimental]) |
338 | + self.factory.makeSnappySeries( |
339 | + usable_distro_series=[old, experimental]) |
340 | branch = self.factory.makeAnyBranch() |
341 | with person_logged_in(self.person): |
342 | view = create_initialized_view(branch, "+new-snap") |
343 | - self.assertEqual(development, view.initial_values["distro_series"]) |
344 | + self.assertThat( |
345 | + view.initial_values["store_distro_series"], |
346 | + MatchesStructure.byEquality( |
347 | + snappy_series=newest, distro_series=development)) |
348 | |
349 | def test_create_new_snap_not_logged_in(self): |
350 | branch = self.factory.makeAnyBranch() |
351 | @@ -173,14 +185,11 @@ |
352 | no_login=True) |
353 | |
354 | def test_create_new_snap_bzr(self): |
355 | - archive = self.factory.makeArchive() |
356 | - distroseries = self.factory.makeDistroSeries( |
357 | - distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT) |
358 | branch = self.factory.makeAnyBranch() |
359 | source_display = branch.display_name |
360 | browser = self.getViewBrowser( |
361 | branch, view_name="+new-snap", user=self.person) |
362 | - browser.getControl("Name").value = "snap-name" |
363 | + browser.getControl(name="field.name").value = "snap-name" |
364 | browser.getControl("Create snap package").click() |
365 | |
366 | content = find_main_content(browser.contents) |
367 | @@ -189,21 +198,22 @@ |
368 | "Test Person", MatchesPickerText(content, "edit-owner")) |
369 | self.assertThat( |
370 | "Distribution series:\n%s\nEdit snap package" % |
371 | - distroseries.fullseriesname, |
372 | + self.distroseries.fullseriesname, |
373 | MatchesTagText(content, "distro_series")) |
374 | self.assertThat( |
375 | "Source:\n%s\nEdit snap package" % source_display, |
376 | MatchesTagText(content, "source")) |
377 | + self.assertThat( |
378 | + "Builds of this snap package are not automatically uploaded to " |
379 | + "the store.\nEdit snap package", |
380 | + MatchesTagText(content, "store_upload")) |
381 | |
382 | def test_create_new_snap_git(self): |
383 | - archive = self.factory.makeArchive() |
384 | - distroseries = self.factory.makeDistroSeries( |
385 | - distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT) |
386 | [git_ref] = self.factory.makeGitRefs() |
387 | source_display = git_ref.display_name |
388 | browser = self.getViewBrowser( |
389 | git_ref, view_name="+new-snap", user=self.person) |
390 | - browser.getControl("Name").value = "snap-name" |
391 | + browser.getControl(name="field.name").value = "snap-name" |
392 | browser.getControl("Create snap package").click() |
393 | |
394 | content = find_main_content(browser.contents) |
395 | @@ -212,11 +222,15 @@ |
396 | "Test Person", MatchesPickerText(content, "edit-owner")) |
397 | self.assertThat( |
398 | "Distribution series:\n%s\nEdit snap package" % |
399 | - distroseries.fullseriesname, |
400 | + self.distroseries.fullseriesname, |
401 | MatchesTagText(content, "distro_series")) |
402 | self.assertThat( |
403 | "Source:\n%s\nEdit snap package" % source_display, |
404 | MatchesTagText(content, "source")) |
405 | + self.assertThat( |
406 | + "Builds of this snap package are not automatically uploaded to " |
407 | + "the store.\nEdit snap package", |
408 | + MatchesTagText(content, "store_upload")) |
409 | |
410 | def test_create_new_snap_users_teams_as_owner_options(self): |
411 | # Teams that the user is in are options for the snap package owner. |
412 | @@ -231,12 +245,12 @@ |
413 | sorted(str(option) for option in options)) |
414 | |
415 | def test_create_new_snap_public(self): |
416 | - # Public owner implies in public snap. |
417 | + # Public owner implies public snap. |
418 | branch = self.factory.makeAnyBranch() |
419 | |
420 | browser = self.getViewBrowser( |
421 | branch, view_name="+new-snap", user=self.person) |
422 | - browser.getControl("Name").value = "public-snap" |
423 | + browser.getControl(name="field.name").value = "public-snap" |
424 | browser.getControl("Create snap package").click() |
425 | |
426 | content = find_main_content(browser.contents) |
427 | @@ -272,7 +286,7 @@ |
428 | |
429 | browser = self.getViewBrowser( |
430 | branch, view_name="+new-snap", user=self.person) |
431 | - browser.getControl("Name").value = "private-snap" |
432 | + browser.getControl(name="field.name").value = "private-snap" |
433 | browser.getControl("Owner").value = ['super-private'] |
434 | browser.getControl("Create snap package").click() |
435 | |
436 | @@ -283,6 +297,60 @@ |
437 | extract_text(find_tag_by_id(browser.contents, "privacy")) |
438 | ) |
439 | |
440 | + def test_create_new_snap_store_upload(self): |
441 | + # Creating a new snap and asking for it to be automatically uploaded |
442 | + # to the store sets all the appropriate fields and redirects to SSO |
443 | + # for authorization. |
444 | + branch = self.factory.makeAnyBranch() |
445 | + view_url = canonical_url(branch, view_name="+new-snap") |
446 | + browser = self.getNonRedirectingBrowser(url=view_url, user=self.person) |
447 | + browser.getControl(name="field.name").value = "snap-name" |
448 | + browser.getControl("Automatically upload to store").selected = True |
449 | + browser.getControl("Registered store package name").value = ( |
450 | + "store-name") |
451 | + root_macaroon = Macaroon() |
452 | + root_macaroon.add_third_party_caveat( |
453 | + urlsplit(config.launchpad.openid_provider_root).netloc, "", |
454 | + "dummy") |
455 | + root_macaroon_raw = root_macaroon.serialize() |
456 | + |
457 | + @all_requests |
458 | + def handler(url, request): |
459 | + self.request = request |
460 | + return { |
461 | + "status_code": 200, |
462 | + "content": {"macaroon": root_macaroon_raw}, |
463 | + } |
464 | + |
465 | + self.pushConfig("snappy", store_url="http://sca.example/") |
466 | + with HTTMock(handler): |
467 | + redirection = self.assertRaises( |
468 | + HTTPError, browser.getControl("Create snap package").click) |
469 | + login_person(self.person) |
470 | + snap = getUtility(ISnapSet).getByName(self.person, u"snap-name") |
471 | + self.assertThat(snap, MatchesStructure.byEquality( |
472 | + owner=self.person, distro_series=self.distroseries, |
473 | + name=u"snap-name", source=branch, store_upload=True, |
474 | + store_series=self.snappyseries, store_name=u"store-name", |
475 | + store_secrets={"root": root_macaroon_raw})) |
476 | + self.assertThat(self.request, MatchesStructure.byEquality( |
477 | + url="http://sca.example/dev/api/acl/", method="POST")) |
478 | + expected_body = { |
479 | + "packages": [{ |
480 | + "name": "store-name", |
481 | + "series": self.snappyseries.name, |
482 | + }], |
483 | + "permissions": ["package_upload"], |
484 | + } |
485 | + self.assertEqual(expected_body, json.loads(self.request.body)) |
486 | + self.assertEqual(303, redirection.code) |
487 | + self.assertEqual( |
488 | + canonical_url(snap, rootsite="code") + |
489 | + "/+authorize/+login?field.callback=on&" |
490 | + "macaroon_caveat_id=dummy&" |
491 | + "discharge_macaroon_field=field.discharge_macaroon", |
492 | + redirection.hdrs["Location"]) |
493 | + |
494 | |
495 | class TestSnapAdminView(BrowserTestCase): |
496 | |
497 | @@ -372,27 +440,53 @@ |
498 | self.useFixture(FakeLogger()) |
499 | self.person = self.factory.makePerson( |
500 | name="test-person", displayname="Test Person") |
501 | + self.distroseries = self.factory.makeUbuntuDistroSeries( |
502 | + version="13.10") |
503 | + with admin_logged_in(): |
504 | + self.snappyseries = self.factory.makeSnappySeries( |
505 | + usable_distro_series=[self.distroseries]) |
506 | + |
507 | + def test_initial_store_series(self): |
508 | + # The initial store_series is the newest that is usable for the |
509 | + # selected distroseries. |
510 | + development = self.factory.makeUbuntuDistroSeries( |
511 | + version="14.10", status=SeriesStatus.DEVELOPMENT) |
512 | + experimental = self.factory.makeUbuntuDistroSeries( |
513 | + version="15.04", status=SeriesStatus.EXPERIMENTAL) |
514 | + with admin_logged_in(): |
515 | + self.factory.makeSnappySeries( |
516 | + usable_distro_series=[development, experimental]) |
517 | + newest = self.factory.makeSnappySeries( |
518 | + usable_distro_series=[development]) |
519 | + self.factory.makeSnappySeries(usable_distro_series=[experimental]) |
520 | + snap = self.factory.makeSnap(distroseries=development) |
521 | + with person_logged_in(self.person): |
522 | + view = create_initialized_view(snap, "+edit") |
523 | + self.assertThat( |
524 | + view.initial_values["store_distro_series"], |
525 | + MatchesStructure.byEquality( |
526 | + snappy_series=newest, distro_series=development)) |
527 | |
528 | def test_edit_snap(self): |
529 | - archive = self.factory.makeArchive() |
530 | - old_series = self.factory.makeDistroSeries( |
531 | - distribution=archive.distribution, status=SeriesStatus.CURRENT) |
532 | + old_series = self.factory.makeUbuntuDistroSeries() |
533 | old_branch = self.factory.makeAnyBranch() |
534 | snap = self.factory.makeSnap( |
535 | registrant=self.person, owner=self.person, distroseries=old_series, |
536 | branch=old_branch) |
537 | self.factory.makeTeam( |
538 | name="new-team", displayname="New Team", members=[self.person]) |
539 | - new_series = self.factory.makeDistroSeries( |
540 | - distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT) |
541 | + new_series = self.factory.makeUbuntuDistroSeries() |
542 | + with admin_logged_in(): |
543 | + new_snappy_series = self.factory.makeSnappySeries( |
544 | + usable_distro_series=[new_series]) |
545 | [new_git_ref] = self.factory.makeGitRefs() |
546 | |
547 | browser = self.getViewBrowser(snap, user=self.person) |
548 | browser.getLink("Edit snap package").click() |
549 | browser.getControl("Owner").value = ["new-team"] |
550 | - browser.getControl("Name").value = "new-name" |
551 | - browser.getControl(name="field.distro_series").value = [ |
552 | - str(new_series.id)] |
553 | + browser.getControl(name="field.name").value = "new-name" |
554 | + browser.getControl(name="field.store_distro_series").value = [ |
555 | + "ubuntu/%s/%s" % (new_series.name, new_snappy_series.name)] |
556 | browser.getControl("Git", index=0).click() |
557 | browser.getControl("Git repository").value = ( |
558 | new_git_ref.repository.identity) |
559 | @@ -409,6 +503,10 @@ |
560 | self.assertThat( |
561 | "Source:\n%s\nEdit snap package" % new_git_ref.display_name, |
562 | MatchesTagText(content, "source")) |
563 | + self.assertThat( |
564 | + "Builds of this snap package are not automatically uploaded to " |
565 | + "the store.\nEdit snap package", |
566 | + MatchesTagText(content, "store_upload")) |
567 | |
568 | def test_edit_snap_sets_date_last_modified(self): |
569 | # Editing a snap package sets the date_last_modified property. |
570 | @@ -432,7 +530,7 @@ |
571 | registrant=self.person, owner=self.person, name=u"two") |
572 | browser = self.getViewBrowser(snap, user=self.person) |
573 | browser.getLink("Edit snap package").click() |
574 | - browser.getControl("Name").value = "two" |
575 | + browser.getControl(name="field.name").value = "two" |
576 | browser.getControl("Update snap package").click() |
577 | self.assertEqual( |
578 | "There is already a snap package owned by Test Person with this " |
579 | @@ -448,6 +546,8 @@ |
580 | self.factory.makeDistroArchSeries( |
581 | distroseries=distroseries, architecturetag=name, |
582 | processor=processor) |
583 | + with admin_logged_in(): |
584 | + self.factory.makeSnappySeries(usable_distro_series=[distroseries]) |
585 | return distroseries |
586 | |
587 | def assertSnapProcessors(self, snap, names): |
588 | @@ -571,6 +671,52 @@ |
589 | login_person(self.person) |
590 | self.assertSnapProcessors(snap, ["386", "armhf"]) |
591 | |
592 | + def test_edit_store_upload(self): |
593 | + # Changing store upload settings on a snap sets all the appropriate |
594 | + # fields and redirects to SSO for reauthorization. |
595 | + snap = self.factory.makeSnap( |
596 | + registrant=self.person, owner=self.person, |
597 | + distroseries=self.distroseries, store_upload=True, |
598 | + store_series=self.snappyseries, store_name=u"one") |
599 | + view_url = canonical_url(snap, view_name="+edit") |
600 | + browser = self.getNonRedirectingBrowser(url=view_url, user=self.person) |
601 | + browser.getControl("Registered store package name").value = "two" |
602 | + root_macaroon = Macaroon() |
603 | + root_macaroon.add_third_party_caveat( |
604 | + urlsplit(config.launchpad.openid_provider_root).netloc, "", |
605 | + "dummy") |
606 | + root_macaroon_raw = root_macaroon.serialize() |
607 | + |
608 | + @all_requests |
609 | + def handler(url, request): |
610 | + self.request = request |
611 | + return { |
612 | + "status_code": 200, |
613 | + "content": {"macaroon": root_macaroon_raw}, |
614 | + } |
615 | + |
616 | + self.pushConfig("snappy", store_url="http://sca.example/") |
617 | + with HTTMock(handler): |
618 | + redirection = self.assertRaises( |
619 | + HTTPError, browser.getControl("Update snap package").click) |
620 | + login_person(self.person) |
621 | + self.assertThat(snap, MatchesStructure.byEquality( |
622 | + store_name=u"two", store_secrets={"root": root_macaroon_raw})) |
623 | + self.assertThat(self.request, MatchesStructure.byEquality( |
624 | + url="http://sca.example/dev/api/acl/", method="POST")) |
625 | + expected_body = { |
626 | + "packages": [{"name": "two", "series": self.snappyseries.name}], |
627 | + "permissions": ["package_upload"], |
628 | + } |
629 | + self.assertEqual(expected_body, json.loads(self.request.body)) |
630 | + self.assertEqual(303, redirection.code) |
631 | + self.assertEqual( |
632 | + canonical_url(snap) + |
633 | + "/+authorize/+login?field.callback=on&" |
634 | + "macaroon_caveat_id=dummy&" |
635 | + "discharge_macaroon_field=field.discharge_macaroon", |
636 | + redirection.hdrs["Location"]) |
637 | + |
638 | |
639 | class TestSnapAuthorizeView(BrowserTestCase): |
640 | |
641 | @@ -839,6 +985,8 @@ |
642 | Owner: Test Person |
643 | Distribution series: Ubuntu Shiny |
644 | Source: lp://dev/~test-person/\\+junk/snap-branch |
645 | + Builds of this snap package are not automatically uploaded to |
646 | + the store. |
647 | Latest builds |
648 | Status When complete Architecture Archive |
649 | Successfully built 30 minutes ago i386 |
650 | @@ -860,6 +1008,8 @@ |
651 | Owner: Test Person |
652 | Distribution series: Ubuntu Shiny |
653 | Source: ~test-person/\\+git/snap-repository:master |
654 | + Builds of this snap package are not automatically uploaded to |
655 | + the store. |
656 | Latest builds |
657 | Status When complete Architecture Archive |
658 | Successfully built 30 minutes ago i386 |
659 | |
660 | === modified file 'lib/lp/snappy/interfaces/snap.py' |
661 | --- lib/lp/snappy/interfaces/snap.py 2016-05-13 15:32:22 +0000 |
662 | +++ lib/lp/snappy/interfaces/snap.py 2016-05-27 10:39:59 +0000 |
663 | @@ -86,7 +86,10 @@ |
664 | PublicPersonChoice, |
665 | ) |
666 | from lp.services.webhooks.interfaces import IWebhookTarget |
667 | -from lp.snappy.interfaces.snappyseries import ISnappySeries |
668 | +from lp.snappy.interfaces.snappyseries import ( |
669 | + ISnappyDistroSeries, |
670 | + ISnappySeries, |
671 | + ) |
672 | from lp.soyuz.interfaces.archive import IArchive |
673 | from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries |
674 | |
675 | @@ -389,6 +392,11 @@ |
676 | "The series in which this snap package should be published in the " |
677 | "store.")) |
678 | |
679 | + store_distro_series = ReferenceChoice( |
680 | + title=_("Store and distro series"), |
681 | + schema=ISnappyDistroSeries, vocabulary="SnappyDistroSeries", |
682 | + required=False, readonly=False) |
683 | + |
684 | store_name = TextLine( |
685 | title=_("Registered store package name"), |
686 | required=False, readonly=False, |
687 | |
688 | === modified file 'lib/lp/snappy/interfaces/snappyseries.py' |
689 | --- lib/lp/snappy/interfaces/snappyseries.py 2016-05-06 11:55:53 +0000 |
690 | +++ lib/lp/snappy/interfaces/snappyseries.py 2016-05-27 10:39:59 +0000 |
691 | @@ -190,3 +190,6 @@ |
692 | |
693 | def getByBothSeries(snappy_series, distro_series): |
694 | """Return a `SnappyDistroSeries` for this pair of series, or None.""" |
695 | + |
696 | + def getAll(): |
697 | + """Return all `SnappyDistroSeries`.""" |
698 | |
699 | === modified file 'lib/lp/snappy/model/snap.py' |
700 | --- lib/lp/snappy/model/snap.py 2016-05-17 12:47:34 +0000 |
701 | +++ lib/lp/snappy/model/snap.py 2016-05-27 10:39:59 +0000 |
702 | @@ -102,6 +102,7 @@ |
703 | SnapPrivateFeatureDisabled, |
704 | ) |
705 | from lp.snappy.interfaces.snapbuild import ISnapBuildSet |
706 | +from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet |
707 | from lp.snappy.model.snapbuild import SnapBuild |
708 | from lp.soyuz.interfaces.archive import ArchiveDisabled |
709 | from lp.soyuz.model.archive import ( |
710 | @@ -291,6 +292,18 @@ |
711 | or not self.require_virtualized))] |
712 | |
713 | @property |
714 | + def store_distro_series(self): |
715 | + if self.store_series is None: |
716 | + return None |
717 | + return getUtility(ISnappyDistroSeriesSet).getByBothSeries( |
718 | + self.store_series, self.distro_series) |
719 | + |
720 | + @store_distro_series.setter |
721 | + def store_distro_series(self, value): |
722 | + self.distro_series = value.distro_series |
723 | + self.store_series = value.snappy_series |
724 | + |
725 | + @property |
726 | def can_upload_to_store(self): |
727 | return ( |
728 | config.snappy.store_upload_url is not None and |
729 | |
730 | === modified file 'lib/lp/snappy/model/snappyseries.py' |
731 | --- lib/lp/snappy/model/snappyseries.py 2016-05-09 13:24:10 +0000 |
732 | +++ lib/lp/snappy/model/snappyseries.py 2016-05-27 10:39:59 +0000 |
733 | @@ -182,3 +182,7 @@ |
734 | SnappyDistroSeries, |
735 | SnappyDistroSeries.snappy_series == snappy_series, |
736 | SnappyDistroSeries.distro_series == distro_series).one() |
737 | + |
738 | + def getAll(self): |
739 | + """See `ISnappyDistroSeriesSet`.""" |
740 | + return IStore(SnappyDistroSeries).find(SnappyDistroSeries) |
741 | |
742 | === modified file 'lib/lp/snappy/templates/snap-edit.pt' |
743 | --- lib/lp/snappy/templates/snap-edit.pt 2016-02-04 00:45:12 +0000 |
744 | +++ lib/lp/snappy/templates/snap-edit.pt 2016-05-27 10:39:59 +0000 |
745 | @@ -25,10 +25,28 @@ |
746 | <tal:widget define="widget nocall:view/widgets/name"> |
747 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
748 | </tal:widget> |
749 | - <tal:widget define="widget nocall:view/widgets/distro_series"> |
750 | + <tal:widget define="widget nocall:view/widgets/store_distro_series"> |
751 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
752 | </tal:widget> |
753 | |
754 | + <tr tal:condition="view/has_snappy_distro_series"> |
755 | + <td> |
756 | + <tal:widget define="widget nocall:view/widgets/store_upload"> |
757 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
758 | + </tal:widget> |
759 | + <table class="subordinate"> |
760 | + <tal:widget define="widget nocall:view/widgets/store_name"> |
761 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
762 | + </tal:widget> |
763 | + </table> |
764 | + <p class="formHelp"> |
765 | + If you change any settings related to automatically uploading |
766 | + builds of this snap to the store, then the login service will |
767 | + prompt you to authorize this request. |
768 | + </p> |
769 | + </td> |
770 | + </tr> |
771 | + |
772 | <tr> |
773 | <td> |
774 | <div> |
775 | |
776 | === modified file 'lib/lp/snappy/templates/snap-index.pt' |
777 | --- lib/lp/snappy/templates/snap-index.pt 2016-05-14 00:25:07 +0000 |
778 | +++ lib/lp/snappy/templates/snap-index.pt 2016-05-27 10:39:59 +0000 |
779 | @@ -63,6 +63,28 @@ |
780 | </dl> |
781 | </div> |
782 | |
783 | + <div id="store_upload" class="two-column-list" |
784 | + tal:condition="context/store_upload"> |
785 | + <dl id="store_series"> |
786 | + <dt>Store series:</dt> |
787 | + <dd> |
788 | + <a tal:replace="structure context/store_series/fmt:link"/> |
789 | + <a tal:replace="structure view/menu:overview/edit/fmt:icon"/> |
790 | + </dd> |
791 | + </dl> |
792 | + <dl id="store_name"> |
793 | + <dt>Registered store package name:</dt> |
794 | + <dd> |
795 | + <span tal:content="context/store_name"/> |
796 | + <a tal:replace="structure view/menu:overview/edit/fmt:icon"/> |
797 | + </dd> |
798 | + </dl> |
799 | + </div> |
800 | + <p id="store_upload" tal:condition="not: context/store_upload"> |
801 | + Builds of this snap package are not automatically uploaded to the store. |
802 | + <a tal:replace="structure view/menu:overview/edit/fmt:icon"/> |
803 | + </p> |
804 | + |
805 | <h2>Latest builds</h2> |
806 | <table id="latest-builds-listing" class="listing" |
807 | style="margin-bottom: 1em;"> |
808 | |
809 | === modified file 'lib/lp/snappy/templates/snap-new.pt' |
810 | --- lib/lp/snappy/templates/snap-new.pt 2016-02-04 00:45:12 +0000 |
811 | +++ lib/lp/snappy/templates/snap-new.pt 2016-05-27 10:39:59 +0000 |
812 | @@ -28,9 +28,27 @@ |
813 | <tal:widget define="widget nocall:view/widgets/owner"> |
814 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
815 | </tal:widget> |
816 | - <tal:widget define="widget nocall:view/widgets/distro_series"> |
817 | + <tal:widget define="widget nocall:view/widgets/store_distro_series"> |
818 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
819 | </tal:widget> |
820 | + |
821 | + <tr tal:condition="view/has_snappy_distro_series"> |
822 | + <td> |
823 | + <tal:widget define="widget nocall:view/widgets/store_upload"> |
824 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
825 | + </tal:widget> |
826 | + <table class="subordinate"> |
827 | + <tal:widget define="widget nocall:view/widgets/store_name"> |
828 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
829 | + </tal:widget> |
830 | + </table> |
831 | + <p class="formHelp"> |
832 | + If you ask Launchpad to automatically upload builds of this |
833 | + snap to the store on your behalf, then the login service |
834 | + will prompt you to authorize this request. |
835 | + </p> |
836 | + </td> |
837 | + </tr> |
838 | </table> |
839 | </metal:formbody> |
840 | </div> |
841 | |
842 | === modified file 'lib/lp/snappy/tests/test_snappyseries.py' |
843 | --- lib/lp/snappy/tests/test_snappyseries.py 2016-05-06 12:08:29 +0000 |
844 | +++ lib/lp/snappy/tests/test_snappyseries.py 2016-05-27 10:39:59 +0000 |
845 | @@ -277,3 +277,20 @@ |
846 | self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[0], dses[1])) |
847 | self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[0])) |
848 | self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[1])) |
849 | + |
850 | + def test_getAll(self): |
851 | + dses = [self.factory.makeDistroSeries() for _ in range(2)] |
852 | + snappy_serieses = [self.factory.makeSnappySeries() for _ in range(2)] |
853 | + snappy_serieses[0].usable_distro_series = dses |
854 | + snappy_serieses[1].usable_distro_series = [dses[0]] |
855 | + sds_set = getUtility(ISnappyDistroSeriesSet) |
856 | + self.assertThat( |
857 | + sds_set.getAll(), |
858 | + MatchesSetwise( |
859 | + MatchesStructure.byEquality( |
860 | + snappy_series=snappy_serieses[0], distro_series=dses[0]), |
861 | + MatchesStructure.byEquality( |
862 | + snappy_series=snappy_serieses[0], distro_series=dses[1]), |
863 | + MatchesStructure.byEquality( |
864 | + snappy_series=snappy_serieses[1], distro_series=dses[0]), |
865 | + )) |