Merge lp:~cjwatson/launchpad/snap-store-add-edit-views into lp:launchpad

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
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
=== modified file 'lib/lp/app/browser/configure.zcml'
--- lib/lp/app/browser/configure.zcml 2015-08-07 10:12:38 +0000
+++ lib/lp/app/browser/configure.zcml 2016-05-27 10:39:59 +0000
@@ -853,6 +853,12 @@
853 name="fmt"853 name="fmt"
854 />854 />
855 <adapter855 <adapter
856 for="lp.snappy.interfaces.snappyseries.ISnappySeries"
857 provides="zope.traversing.interfaces.IPathAdapter"
858 factory="lp.app.browser.tales.SnappySeriesFormatterAPI"
859 name="fmt"
860 />
861 <adapter
856 for="lp.blueprints.interfaces.specification.ISpecification"862 for="lp.blueprints.interfaces.specification.ISpecification"
857 provides="zope.traversing.interfaces.IPathAdapter"863 provides="zope.traversing.interfaces.IPathAdapter"
858 factory="lp.app.browser.tales.SpecificationFormatterAPI"864 factory="lp.app.browser.tales.SpecificationFormatterAPI"
859865
=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py 2016-02-04 04:39:30 +0000
+++ lib/lp/app/browser/tales.py 2016-05-27 10:39:59 +0000
@@ -1872,6 +1872,15 @@
1872 'owner': self._context.owner.displayname}1872 'owner': self._context.owner.displayname}
18731873
18741874
1875class SnappySeriesFormatterAPI(CustomizableFormatter):
1876 """Adapter providing fmt support for ISnappySeries objects."""
1877
1878 _link_summary_template = _('%(title)s')
1879
1880 def _link_summary_values(self):
1881 return {'title': self._context.title}
1882
1883
1875class SpecificationFormatterAPI(CustomizableFormatter):1884class SpecificationFormatterAPI(CustomizableFormatter):
1876 """Adapter providing fmt support for Specification objects"""1885 """Adapter providing fmt support for Specification objects"""
18771886
18781887
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2016-05-24 04:45:38 +0000
+++ lib/lp/snappy/browser/snap.py 2016-05-27 10:39:59 +0000
@@ -26,6 +26,7 @@
26 )26 )
27from pymacaroons import Macaroon27from pymacaroons import Macaroon
28from zope.component import getUtility28from zope.component import getUtility
29from zope.error.interfaces import IErrorReportingUtility
29from zope.interface import Interface30from zope.interface import Interface
30from zope.schema import (31from zope.schema import (
31 Choice,32 Choice,
@@ -87,7 +88,11 @@
87 SnapPrivateFeatureDisabled,88 SnapPrivateFeatureDisabled,
88 )89 )
89from lp.snappy.interfaces.snapbuild import ISnapBuildSet90from lp.snappy.interfaces.snapbuild import ISnapBuildSet
90from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient91from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet
92from lp.snappy.interfaces.snapstoreclient import (
93 BadRequestPackageUploadResponse,
94 ISnapStoreClient,
95 )
91from lp.soyuz.browser.archive import EnableProcessorsMixin96from lp.soyuz.browser.archive import EnableProcessorsMixin
92from lp.soyuz.browser.build import get_build_by_id_str97from lp.soyuz.browser.build import get_build_by_id_str
93from lp.soyuz.interfaces.archive import IArchive98from lp.soyuz.interfaces.archive import IArchive
@@ -296,9 +301,11 @@
296 'name',301 'name',
297 'private',302 'private',
298 'require_virtualized',303 'require_virtualized',
304 'store_upload',
299 ])305 ])
300 distro_series = Choice(306 store_distro_series = Choice(
301 vocabulary='BuildableDistroSeries', title=u'Distribution series')307 vocabulary='BuildableSnappyDistroSeries', required=True,
308 title=u'Series')
302 vcs = Choice(vocabulary=VCSType, required=True, title=u'VCS')309 vcs = Choice(vocabulary=VCSType, required=True, title=u'VCS')
303310
304 # Each of these is only required if vcs has an appropriate value. Later311 # Each of these is only required if vcs has an appropriate value. Later
@@ -306,15 +313,44 @@
306 branch = copy_field(ISnap['branch'], required=True)313 branch = copy_field(ISnap['branch'], required=True)
307 git_ref = copy_field(ISnap['git_ref'], required=True)314 git_ref = copy_field(ISnap['git_ref'], required=True)
308315
309316 # These are only required if store_upload is True. Later validation
310class SnapAddView(LaunchpadFormView):317 # takes care of adjusting the required attribute.
318 store_name = copy_field(ISnap['store_name'], required=True)
319
320
321def log_oops(error, request):
322 """Log an oops report without raising an error."""
323 info = (error.__class__, error, None)
324 getUtility(IErrorReportingUtility).raising(info, request)
325
326
327class SnapAuthorizeMixin:
328
329 def requestAuthorization(self, snap):
330 try:
331 self.next_url = SnapAuthorizeView.requestAuthorization(
332 snap, self.request)
333 except BadRequestPackageUploadResponse as e:
334 self.setFieldError(
335 'store_upload',
336 'Cannot get permission from the store to upload this package.')
337 log_oops(e, self.request)
338
339
340class SnapAddView(LaunchpadFormView, SnapAuthorizeMixin):
311 """View for creating snap packages."""341 """View for creating snap packages."""
312342
313 page_title = label = 'Create a new snap package'343 page_title = label = 'Create a new snap package'
314344
315 schema = ISnapEditSchema345 schema = ISnapEditSchema
316 field_names = ['owner', 'name', 'distro_series']346 field_names = [
317 custom_widget('distro_series', LaunchpadRadioWidget)347 'owner',
348 'name',
349 'store_distro_series',
350 'store_upload',
351 'store_name',
352 ]
353 custom_widget('store_distro_series', LaunchpadRadioWidget)
318354
319 def initialize(self):355 def initialize(self):
320 """See `LaunchpadView`."""356 """See `LaunchpadView`."""
@@ -340,13 +376,28 @@
340 # accidentally selecting ubuntu-rtm/14.09 or similar.376 # accidentally selecting ubuntu-rtm/14.09 or similar.
341 # ubuntu.currentseries will always be in BuildableDistroSeries.377 # ubuntu.currentseries will always be in BuildableDistroSeries.
342 series = getUtility(ILaunchpadCelebrities).ubuntu.currentseries378 series = getUtility(ILaunchpadCelebrities).ubuntu.currentseries
379 sds_set = getUtility(ISnappyDistroSeriesSet)
343 return {380 return {
344 'owner': self.user,381 'owner': self.user,
345 'distro_series': series,382 'store_distro_series': sds_set.getByDistroSeries(series).first(),
346 }383 }
347384
385 @property
386 def has_snappy_distro_series(self):
387 return not getUtility(ISnappyDistroSeriesSet).getAll().is_empty()
388
389 def validate_widgets(self, data, names=None):
390 """See `LaunchpadFormView`."""
391 if self.widgets.get('store_upload') is not None:
392 # Set widgets as required or optional depending on the
393 # store_upload field.
394 super(SnapAddView, self).validate_widgets(data, ['store_upload'])
395 store_upload = data.get('store_upload', False)
396 self.widgets['store_name'].context.required = store_upload
397 super(SnapAddView, self).validate_widgets(data, names=names)
398
348 @action('Create snap package', name='create')399 @action('Create snap package', name='create')
349 def request_action(self, action, data):400 def create_action(self, action, data):
350 if IGitRef.providedBy(self.context):401 if IGitRef.providedBy(self.context):
351 kwargs = {'git_ref': self.context}402 kwargs = {'git_ref': self.context}
352 else:403 else:
@@ -354,9 +405,15 @@
354 private = not getUtility(405 private = not getUtility(
355 ISnapSet).isValidPrivacy(False, data['owner'], **kwargs)406 ISnapSet).isValidPrivacy(False, data['owner'], **kwargs)
356 snap = getUtility(ISnapSet).new(407 snap = getUtility(ISnapSet).new(
357 self.user, data['owner'], data['distro_series'], data['name'],408 self.user, data['owner'],
358 private=private, **kwargs)409 data['store_distro_series'].distro_series, data['name'],
359 self.next_url = canonical_url(snap)410 private=private, store_upload=data['store_upload'],
411 store_series=data['store_distro_series'].snappy_series,
412 store_name=data['store_name'], **kwargs)
413 if data['store_upload']:
414 self.requestAuthorization(snap)
415 else:
416 self.next_url = canonical_url(snap)
360417
361 def validate(self, data):418 def validate(self, data):
362 super(SnapAddView, self).validate(data)419 super(SnapAddView, self).validate(data)
@@ -370,7 +427,7 @@
370 'name.' % owner.displayname)427 'name.' % owner.displayname)
371428
372429
373class BaseSnapEditView(LaunchpadEditFormView):430class BaseSnapEditView(LaunchpadEditFormView, SnapAuthorizeMixin):
374431
375 schema = ISnapEditSchema432 schema = ISnapEditSchema
376433
@@ -388,6 +445,10 @@
388 render_radio_widget_part(widget, value, current_value)445 render_radio_widget_part(widget, value, current_value)
389 for value in (VCSType.BZR, VCSType.GIT)]446 for value in (VCSType.BZR, VCSType.GIT)]
390447
448 @property
449 def has_snappy_distro_series(self):
450 return not getUtility(ISnappyDistroSeriesSet).getAll().is_empty()
451
391 def validate_widgets(self, data, names=None):452 def validate_widgets(self, data, names=None):
392 """See `LaunchpadFormView`."""453 """See `LaunchpadFormView`."""
393 if self.widgets.get('vcs') is not None:454 if self.widgets.get('vcs') is not None:
@@ -403,8 +464,29 @@
403 self.widgets['git_ref'].context.required = True464 self.widgets['git_ref'].context.required = True
404 else:465 else:
405 raise AssertionError("Unknown branch type %s" % vcs)466 raise AssertionError("Unknown branch type %s" % vcs)
467 if self.widgets.get('store_upload') is not None:
468 # Set widgets as required or optional depending on the
469 # store_upload field.
470 super(BaseSnapEditView, self).validate_widgets(
471 data, ['store_upload'])
472 store_upload = data.get('store_upload', False)
473 self.widgets['store_name'].context.required = store_upload
406 super(BaseSnapEditView, self).validate_widgets(data, names=names)474 super(BaseSnapEditView, self).validate_widgets(data, names=names)
407475
476 def _needStoreReauth(self, data):
477 """Does this change require reauthorizing to the store?"""
478 store_upload = data.get('store_upload', False)
479 store_distro_series = data.get('store_distro_series')
480 store_name = data.get('store_name')
481 if (not store_upload or
482 store_distro_series is None or store_name is None):
483 return False
484 if store_distro_series.snappy_series != self.context.store_series:
485 return True
486 if store_name != self.context.store_name:
487 return True
488 return False
489
408 @action('Update snap package', name='update')490 @action('Update snap package', name='update')
409 def request_action(self, action, data):491 def request_action(self, action, data):
410 vcs = data.pop('vcs', None)492 vcs = data.pop('vcs', None)
@@ -418,8 +500,15 @@
418 self.context.setProcessors(500 self.context.setProcessors(
419 new_processors, check_permissions=True, user=self.user)501 new_processors, check_permissions=True, user=self.user)
420 del data['processors']502 del data['processors']
503 store_upload = data.get('store_upload', False)
504 if not store_upload:
505 data['store_name'] = None
506 need_store_reauth = self._needStoreReauth(data)
421 self.updateContextFromData(data)507 self.updateContextFromData(data)
422 self.next_url = canonical_url(self.context)508 if need_store_reauth:
509 self.requestAuthorization(self.context)
510 else:
511 self.next_url = canonical_url(self.context)
423512
424 @property513 @property
425 def adapters(self):514 def adapters(self):
@@ -462,8 +551,16 @@
462 page_title = 'Edit'551 page_title = 'Edit'
463552
464 field_names = [553 field_names = [
465 'owner', 'name', 'distro_series', 'vcs', 'branch', 'git_ref']554 'owner',
466 custom_widget('distro_series', LaunchpadRadioWidget)555 'name',
556 'store_distro_series',
557 'store_upload',
558 'store_name',
559 'vcs',
560 'branch',
561 'git_ref',
562 ]
563 custom_widget('store_distro_series', LaunchpadRadioWidget)
467 custom_widget('vcs', LaunchpadRadioWidget)564 custom_widget('vcs', LaunchpadRadioWidget)
468 custom_widget('git_ref', GitRefWidget)565 custom_widget('git_ref', GitRefWidget)
469566
@@ -478,11 +575,18 @@
478575
479 @property576 @property
480 def initial_values(self):577 def initial_values(self):
578 initial_values = {}
579 if self.context.store_series is None:
580 # XXX cjwatson 2016-04-26: Remove this case once all existing
581 # Snaps have had a store_series backfilled.
582 sds_set = getUtility(ISnappyDistroSeriesSet)
583 initial_values['store_distro_series'] = sds_set.getByDistroSeries(
584 self.context.distro_series).first()
481 if self.context.git_ref is not None:585 if self.context.git_ref is not None:
482 vcs = VCSType.GIT586 initial_values['vcs'] = VCSType.GIT
483 else:587 else:
484 vcs = VCSType.BZR588 initial_values['vcs'] = VCSType.BZR
485 return {'vcs': vcs}589 return initial_values
486590
487 def validate(self, data):591 def validate(self, data):
488 super(SnapEditView, self).validate(data)592 super(SnapEditView, self).validate(data)
489593
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2016-05-24 04:45:38 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2016-05-27 10:39:59 +0000
@@ -53,6 +53,7 @@
53 )53 )
54from lp.snappy.interfaces.snap import (54from lp.snappy.interfaces.snap import (
55 CannotModifySnapProcessor,55 CannotModifySnapProcessor,
56 ISnapSet,
56 SNAP_FEATURE_FLAG,57 SNAP_FEATURE_FLAG,
57 SNAP_TESTING_FLAGS,58 SNAP_TESTING_FLAGS,
58 SnapFeatureDisabled,59 SnapFeatureDisabled,
@@ -139,7 +140,7 @@
139140
140class TestSnapAddView(BrowserTestCase):141class TestSnapAddView(BrowserTestCase):
141142
142 layer = DatabaseFunctionalLayer143 layer = LaunchpadFunctionalLayer
143144
144 def setUp(self):145 def setUp(self):
145 super(TestSnapAddView, self).setUp()146 super(TestSnapAddView, self).setUp()
@@ -147,24 +148,35 @@
147 self.useFixture(FakeLogger())148 self.useFixture(FakeLogger())
148 self.person = self.factory.makePerson(149 self.person = self.factory.makePerson(
149 name="test-person", displayname="Test Person")150 name="test-person", displayname="Test Person")
151 self.distroseries = self.factory.makeUbuntuDistroSeries(
152 version="13.10")
153 with admin_logged_in():
154 self.snappyseries = self.factory.makeSnappySeries(
155 usable_distro_series=[self.distroseries])
150156
151 def test_initial_distroseries(self):157 def test_initial_distroseries(self):
152 # The initial distroseries is the newest that is current or in158 # The initial distroseries is the newest that is current or in
153 # development.159 # development.
154 archive = self.factory.makeArchive(owner=self.person)160 old = self.factory.makeUbuntuDistroSeries(
155 self.factory.makeDistroSeries(161 version="14.04", status=SeriesStatus.DEVELOPMENT)
156 distribution=archive.distribution, version="14.04",162 development = self.factory.makeUbuntuDistroSeries(
157 status=SeriesStatus.DEVELOPMENT)163 version="14.10", status=SeriesStatus.DEVELOPMENT)
158 development = self.factory.makeDistroSeries(164 experimental = self.factory.makeUbuntuDistroSeries(
159 distribution=archive.distribution, version="14.10",165 version="15.04", status=SeriesStatus.EXPERIMENTAL)
160 status=SeriesStatus.DEVELOPMENT)166 with admin_logged_in():
161 self.factory.makeDistroSeries(167 self.factory.makeSnappySeries(
162 distribution=archive.distribution, version="15.04",168 usable_distro_series=[old, development, experimental])
163 status=SeriesStatus.EXPERIMENTAL)169 newest = self.factory.makeSnappySeries(
170 usable_distro_series=[development, experimental])
171 self.factory.makeSnappySeries(
172 usable_distro_series=[old, experimental])
164 branch = self.factory.makeAnyBranch()173 branch = self.factory.makeAnyBranch()
165 with person_logged_in(self.person):174 with person_logged_in(self.person):
166 view = create_initialized_view(branch, "+new-snap")175 view = create_initialized_view(branch, "+new-snap")
167 self.assertEqual(development, view.initial_values["distro_series"])176 self.assertThat(
177 view.initial_values["store_distro_series"],
178 MatchesStructure.byEquality(
179 snappy_series=newest, distro_series=development))
168180
169 def test_create_new_snap_not_logged_in(self):181 def test_create_new_snap_not_logged_in(self):
170 branch = self.factory.makeAnyBranch()182 branch = self.factory.makeAnyBranch()
@@ -173,14 +185,11 @@
173 no_login=True)185 no_login=True)
174186
175 def test_create_new_snap_bzr(self):187 def test_create_new_snap_bzr(self):
176 archive = self.factory.makeArchive()
177 distroseries = self.factory.makeDistroSeries(
178 distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)
179 branch = self.factory.makeAnyBranch()188 branch = self.factory.makeAnyBranch()
180 source_display = branch.display_name189 source_display = branch.display_name
181 browser = self.getViewBrowser(190 browser = self.getViewBrowser(
182 branch, view_name="+new-snap", user=self.person)191 branch, view_name="+new-snap", user=self.person)
183 browser.getControl("Name").value = "snap-name"192 browser.getControl(name="field.name").value = "snap-name"
184 browser.getControl("Create snap package").click()193 browser.getControl("Create snap package").click()
185194
186 content = find_main_content(browser.contents)195 content = find_main_content(browser.contents)
@@ -189,21 +198,22 @@
189 "Test Person", MatchesPickerText(content, "edit-owner"))198 "Test Person", MatchesPickerText(content, "edit-owner"))
190 self.assertThat(199 self.assertThat(
191 "Distribution series:\n%s\nEdit snap package" %200 "Distribution series:\n%s\nEdit snap package" %
192 distroseries.fullseriesname,201 self.distroseries.fullseriesname,
193 MatchesTagText(content, "distro_series"))202 MatchesTagText(content, "distro_series"))
194 self.assertThat(203 self.assertThat(
195 "Source:\n%s\nEdit snap package" % source_display,204 "Source:\n%s\nEdit snap package" % source_display,
196 MatchesTagText(content, "source"))205 MatchesTagText(content, "source"))
206 self.assertThat(
207 "Builds of this snap package are not automatically uploaded to "
208 "the store.\nEdit snap package",
209 MatchesTagText(content, "store_upload"))
197210
198 def test_create_new_snap_git(self):211 def test_create_new_snap_git(self):
199 archive = self.factory.makeArchive()
200 distroseries = self.factory.makeDistroSeries(
201 distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)
202 [git_ref] = self.factory.makeGitRefs()212 [git_ref] = self.factory.makeGitRefs()
203 source_display = git_ref.display_name213 source_display = git_ref.display_name
204 browser = self.getViewBrowser(214 browser = self.getViewBrowser(
205 git_ref, view_name="+new-snap", user=self.person)215 git_ref, view_name="+new-snap", user=self.person)
206 browser.getControl("Name").value = "snap-name"216 browser.getControl(name="field.name").value = "snap-name"
207 browser.getControl("Create snap package").click()217 browser.getControl("Create snap package").click()
208218
209 content = find_main_content(browser.contents)219 content = find_main_content(browser.contents)
@@ -212,11 +222,15 @@
212 "Test Person", MatchesPickerText(content, "edit-owner"))222 "Test Person", MatchesPickerText(content, "edit-owner"))
213 self.assertThat(223 self.assertThat(
214 "Distribution series:\n%s\nEdit snap package" %224 "Distribution series:\n%s\nEdit snap package" %
215 distroseries.fullseriesname,225 self.distroseries.fullseriesname,
216 MatchesTagText(content, "distro_series"))226 MatchesTagText(content, "distro_series"))
217 self.assertThat(227 self.assertThat(
218 "Source:\n%s\nEdit snap package" % source_display,228 "Source:\n%s\nEdit snap package" % source_display,
219 MatchesTagText(content, "source"))229 MatchesTagText(content, "source"))
230 self.assertThat(
231 "Builds of this snap package are not automatically uploaded to "
232 "the store.\nEdit snap package",
233 MatchesTagText(content, "store_upload"))
220234
221 def test_create_new_snap_users_teams_as_owner_options(self):235 def test_create_new_snap_users_teams_as_owner_options(self):
222 # Teams that the user is in are options for the snap package owner.236 # Teams that the user is in are options for the snap package owner.
@@ -231,12 +245,12 @@
231 sorted(str(option) for option in options))245 sorted(str(option) for option in options))
232246
233 def test_create_new_snap_public(self):247 def test_create_new_snap_public(self):
234 # Public owner implies in public snap.248 # Public owner implies public snap.
235 branch = self.factory.makeAnyBranch()249 branch = self.factory.makeAnyBranch()
236250
237 browser = self.getViewBrowser(251 browser = self.getViewBrowser(
238 branch, view_name="+new-snap", user=self.person)252 branch, view_name="+new-snap", user=self.person)
239 browser.getControl("Name").value = "public-snap"253 browser.getControl(name="field.name").value = "public-snap"
240 browser.getControl("Create snap package").click()254 browser.getControl("Create snap package").click()
241255
242 content = find_main_content(browser.contents)256 content = find_main_content(browser.contents)
@@ -272,7 +286,7 @@
272286
273 browser = self.getViewBrowser(287 browser = self.getViewBrowser(
274 branch, view_name="+new-snap", user=self.person)288 branch, view_name="+new-snap", user=self.person)
275 browser.getControl("Name").value = "private-snap"289 browser.getControl(name="field.name").value = "private-snap"
276 browser.getControl("Owner").value = ['super-private']290 browser.getControl("Owner").value = ['super-private']
277 browser.getControl("Create snap package").click()291 browser.getControl("Create snap package").click()
278292
@@ -283,6 +297,60 @@
283 extract_text(find_tag_by_id(browser.contents, "privacy"))297 extract_text(find_tag_by_id(browser.contents, "privacy"))
284 )298 )
285299
300 def test_create_new_snap_store_upload(self):
301 # Creating a new snap and asking for it to be automatically uploaded
302 # to the store sets all the appropriate fields and redirects to SSO
303 # for authorization.
304 branch = self.factory.makeAnyBranch()
305 view_url = canonical_url(branch, view_name="+new-snap")
306 browser = self.getNonRedirectingBrowser(url=view_url, user=self.person)
307 browser.getControl(name="field.name").value = "snap-name"
308 browser.getControl("Automatically upload to store").selected = True
309 browser.getControl("Registered store package name").value = (
310 "store-name")
311 root_macaroon = Macaroon()
312 root_macaroon.add_third_party_caveat(
313 urlsplit(config.launchpad.openid_provider_root).netloc, "",
314 "dummy")
315 root_macaroon_raw = root_macaroon.serialize()
316
317 @all_requests
318 def handler(url, request):
319 self.request = request
320 return {
321 "status_code": 200,
322 "content": {"macaroon": root_macaroon_raw},
323 }
324
325 self.pushConfig("snappy", store_url="http://sca.example/")
326 with HTTMock(handler):
327 redirection = self.assertRaises(
328 HTTPError, browser.getControl("Create snap package").click)
329 login_person(self.person)
330 snap = getUtility(ISnapSet).getByName(self.person, u"snap-name")
331 self.assertThat(snap, MatchesStructure.byEquality(
332 owner=self.person, distro_series=self.distroseries,
333 name=u"snap-name", source=branch, store_upload=True,
334 store_series=self.snappyseries, store_name=u"store-name",
335 store_secrets={"root": root_macaroon_raw}))
336 self.assertThat(self.request, MatchesStructure.byEquality(
337 url="http://sca.example/dev/api/acl/", method="POST"))
338 expected_body = {
339 "packages": [{
340 "name": "store-name",
341 "series": self.snappyseries.name,
342 }],
343 "permissions": ["package_upload"],
344 }
345 self.assertEqual(expected_body, json.loads(self.request.body))
346 self.assertEqual(303, redirection.code)
347 self.assertEqual(
348 canonical_url(snap, rootsite="code") +
349 "/+authorize/+login?field.callback=on&"
350 "macaroon_caveat_id=dummy&"
351 "discharge_macaroon_field=field.discharge_macaroon",
352 redirection.hdrs["Location"])
353
286354
287class TestSnapAdminView(BrowserTestCase):355class TestSnapAdminView(BrowserTestCase):
288356
@@ -372,27 +440,53 @@
372 self.useFixture(FakeLogger())440 self.useFixture(FakeLogger())
373 self.person = self.factory.makePerson(441 self.person = self.factory.makePerson(
374 name="test-person", displayname="Test Person")442 name="test-person", displayname="Test Person")
443 self.distroseries = self.factory.makeUbuntuDistroSeries(
444 version="13.10")
445 with admin_logged_in():
446 self.snappyseries = self.factory.makeSnappySeries(
447 usable_distro_series=[self.distroseries])
448
449 def test_initial_store_series(self):
450 # The initial store_series is the newest that is usable for the
451 # selected distroseries.
452 development = self.factory.makeUbuntuDistroSeries(
453 version="14.10", status=SeriesStatus.DEVELOPMENT)
454 experimental = self.factory.makeUbuntuDistroSeries(
455 version="15.04", status=SeriesStatus.EXPERIMENTAL)
456 with admin_logged_in():
457 self.factory.makeSnappySeries(
458 usable_distro_series=[development, experimental])
459 newest = self.factory.makeSnappySeries(
460 usable_distro_series=[development])
461 self.factory.makeSnappySeries(usable_distro_series=[experimental])
462 snap = self.factory.makeSnap(distroseries=development)
463 with person_logged_in(self.person):
464 view = create_initialized_view(snap, "+edit")
465 self.assertThat(
466 view.initial_values["store_distro_series"],
467 MatchesStructure.byEquality(
468 snappy_series=newest, distro_series=development))
375469
376 def test_edit_snap(self):470 def test_edit_snap(self):
377 archive = self.factory.makeArchive()471 old_series = self.factory.makeUbuntuDistroSeries()
378 old_series = self.factory.makeDistroSeries(
379 distribution=archive.distribution, status=SeriesStatus.CURRENT)
380 old_branch = self.factory.makeAnyBranch()472 old_branch = self.factory.makeAnyBranch()
381 snap = self.factory.makeSnap(473 snap = self.factory.makeSnap(
382 registrant=self.person, owner=self.person, distroseries=old_series,474 registrant=self.person, owner=self.person, distroseries=old_series,
383 branch=old_branch)475 branch=old_branch)
384 self.factory.makeTeam(476 self.factory.makeTeam(
385 name="new-team", displayname="New Team", members=[self.person])477 name="new-team", displayname="New Team", members=[self.person])
386 new_series = self.factory.makeDistroSeries(478 new_series = self.factory.makeUbuntuDistroSeries()
387 distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)479 with admin_logged_in():
480 new_snappy_series = self.factory.makeSnappySeries(
481 usable_distro_series=[new_series])
388 [new_git_ref] = self.factory.makeGitRefs()482 [new_git_ref] = self.factory.makeGitRefs()
389483
390 browser = self.getViewBrowser(snap, user=self.person)484 browser = self.getViewBrowser(snap, user=self.person)
391 browser.getLink("Edit snap package").click()485 browser.getLink("Edit snap package").click()
392 browser.getControl("Owner").value = ["new-team"]486 browser.getControl("Owner").value = ["new-team"]
393 browser.getControl("Name").value = "new-name"487 browser.getControl(name="field.name").value = "new-name"
394 browser.getControl(name="field.distro_series").value = [488 browser.getControl(name="field.store_distro_series").value = [
395 str(new_series.id)]489 "ubuntu/%s/%s" % (new_series.name, new_snappy_series.name)]
396 browser.getControl("Git", index=0).click()490 browser.getControl("Git", index=0).click()
397 browser.getControl("Git repository").value = (491 browser.getControl("Git repository").value = (
398 new_git_ref.repository.identity)492 new_git_ref.repository.identity)
@@ -409,6 +503,10 @@
409 self.assertThat(503 self.assertThat(
410 "Source:\n%s\nEdit snap package" % new_git_ref.display_name,504 "Source:\n%s\nEdit snap package" % new_git_ref.display_name,
411 MatchesTagText(content, "source"))505 MatchesTagText(content, "source"))
506 self.assertThat(
507 "Builds of this snap package are not automatically uploaded to "
508 "the store.\nEdit snap package",
509 MatchesTagText(content, "store_upload"))
412510
413 def test_edit_snap_sets_date_last_modified(self):511 def test_edit_snap_sets_date_last_modified(self):
414 # Editing a snap package sets the date_last_modified property.512 # Editing a snap package sets the date_last_modified property.
@@ -432,7 +530,7 @@
432 registrant=self.person, owner=self.person, name=u"two")530 registrant=self.person, owner=self.person, name=u"two")
433 browser = self.getViewBrowser(snap, user=self.person)531 browser = self.getViewBrowser(snap, user=self.person)
434 browser.getLink("Edit snap package").click()532 browser.getLink("Edit snap package").click()
435 browser.getControl("Name").value = "two"533 browser.getControl(name="field.name").value = "two"
436 browser.getControl("Update snap package").click()534 browser.getControl("Update snap package").click()
437 self.assertEqual(535 self.assertEqual(
438 "There is already a snap package owned by Test Person with this "536 "There is already a snap package owned by Test Person with this "
@@ -448,6 +546,8 @@
448 self.factory.makeDistroArchSeries(546 self.factory.makeDistroArchSeries(
449 distroseries=distroseries, architecturetag=name,547 distroseries=distroseries, architecturetag=name,
450 processor=processor)548 processor=processor)
549 with admin_logged_in():
550 self.factory.makeSnappySeries(usable_distro_series=[distroseries])
451 return distroseries551 return distroseries
452552
453 def assertSnapProcessors(self, snap, names):553 def assertSnapProcessors(self, snap, names):
@@ -571,6 +671,52 @@
571 login_person(self.person)671 login_person(self.person)
572 self.assertSnapProcessors(snap, ["386", "armhf"])672 self.assertSnapProcessors(snap, ["386", "armhf"])
573673
674 def test_edit_store_upload(self):
675 # Changing store upload settings on a snap sets all the appropriate
676 # fields and redirects to SSO for reauthorization.
677 snap = self.factory.makeSnap(
678 registrant=self.person, owner=self.person,
679 distroseries=self.distroseries, store_upload=True,
680 store_series=self.snappyseries, store_name=u"one")
681 view_url = canonical_url(snap, view_name="+edit")
682 browser = self.getNonRedirectingBrowser(url=view_url, user=self.person)
683 browser.getControl("Registered store package name").value = "two"
684 root_macaroon = Macaroon()
685 root_macaroon.add_third_party_caveat(
686 urlsplit(config.launchpad.openid_provider_root).netloc, "",
687 "dummy")
688 root_macaroon_raw = root_macaroon.serialize()
689
690 @all_requests
691 def handler(url, request):
692 self.request = request
693 return {
694 "status_code": 200,
695 "content": {"macaroon": root_macaroon_raw},
696 }
697
698 self.pushConfig("snappy", store_url="http://sca.example/")
699 with HTTMock(handler):
700 redirection = self.assertRaises(
701 HTTPError, browser.getControl("Update snap package").click)
702 login_person(self.person)
703 self.assertThat(snap, MatchesStructure.byEquality(
704 store_name=u"two", store_secrets={"root": root_macaroon_raw}))
705 self.assertThat(self.request, MatchesStructure.byEquality(
706 url="http://sca.example/dev/api/acl/", method="POST"))
707 expected_body = {
708 "packages": [{"name": "two", "series": self.snappyseries.name}],
709 "permissions": ["package_upload"],
710 }
711 self.assertEqual(expected_body, json.loads(self.request.body))
712 self.assertEqual(303, redirection.code)
713 self.assertEqual(
714 canonical_url(snap) +
715 "/+authorize/+login?field.callback=on&"
716 "macaroon_caveat_id=dummy&"
717 "discharge_macaroon_field=field.discharge_macaroon",
718 redirection.hdrs["Location"])
719
574720
575class TestSnapAuthorizeView(BrowserTestCase):721class TestSnapAuthorizeView(BrowserTestCase):
576722
@@ -839,6 +985,8 @@
839 Owner: Test Person985 Owner: Test Person
840 Distribution series: Ubuntu Shiny986 Distribution series: Ubuntu Shiny
841 Source: lp://dev/~test-person/\\+junk/snap-branch987 Source: lp://dev/~test-person/\\+junk/snap-branch
988 Builds of this snap package are not automatically uploaded to
989 the store.
842 Latest builds990 Latest builds
843 Status When complete Architecture Archive991 Status When complete Architecture Archive
844 Successfully built 30 minutes ago i386992 Successfully built 30 minutes ago i386
@@ -860,6 +1008,8 @@
860 Owner: Test Person1008 Owner: Test Person
861 Distribution series: Ubuntu Shiny1009 Distribution series: Ubuntu Shiny
862 Source: ~test-person/\\+git/snap-repository:master1010 Source: ~test-person/\\+git/snap-repository:master
1011 Builds of this snap package are not automatically uploaded to
1012 the store.
863 Latest builds1013 Latest builds
864 Status When complete Architecture Archive1014 Status When complete Architecture Archive
865 Successfully built 30 minutes ago i3861015 Successfully built 30 minutes ago i386
8661016
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2016-05-13 15:32:22 +0000
+++ lib/lp/snappy/interfaces/snap.py 2016-05-27 10:39:59 +0000
@@ -86,7 +86,10 @@
86 PublicPersonChoice,86 PublicPersonChoice,
87 )87 )
88from lp.services.webhooks.interfaces import IWebhookTarget88from lp.services.webhooks.interfaces import IWebhookTarget
89from lp.snappy.interfaces.snappyseries import ISnappySeries89from lp.snappy.interfaces.snappyseries import (
90 ISnappyDistroSeries,
91 ISnappySeries,
92 )
90from lp.soyuz.interfaces.archive import IArchive93from lp.soyuz.interfaces.archive import IArchive
91from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries94from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
9295
@@ -389,6 +392,11 @@
389 "The series in which this snap package should be published in the "392 "The series in which this snap package should be published in the "
390 "store."))393 "store."))
391394
395 store_distro_series = ReferenceChoice(
396 title=_("Store and distro series"),
397 schema=ISnappyDistroSeries, vocabulary="SnappyDistroSeries",
398 required=False, readonly=False)
399
392 store_name = TextLine(400 store_name = TextLine(
393 title=_("Registered store package name"),401 title=_("Registered store package name"),
394 required=False, readonly=False,402 required=False, readonly=False,
395403
=== modified file 'lib/lp/snappy/interfaces/snappyseries.py'
--- lib/lp/snappy/interfaces/snappyseries.py 2016-05-06 11:55:53 +0000
+++ lib/lp/snappy/interfaces/snappyseries.py 2016-05-27 10:39:59 +0000
@@ -190,3 +190,6 @@
190190
191 def getByBothSeries(snappy_series, distro_series):191 def getByBothSeries(snappy_series, distro_series):
192 """Return a `SnappyDistroSeries` for this pair of series, or None."""192 """Return a `SnappyDistroSeries` for this pair of series, or None."""
193
194 def getAll():
195 """Return all `SnappyDistroSeries`."""
193196
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2016-05-17 12:47:34 +0000
+++ lib/lp/snappy/model/snap.py 2016-05-27 10:39:59 +0000
@@ -102,6 +102,7 @@
102 SnapPrivateFeatureDisabled,102 SnapPrivateFeatureDisabled,
103 )103 )
104from lp.snappy.interfaces.snapbuild import ISnapBuildSet104from lp.snappy.interfaces.snapbuild import ISnapBuildSet
105from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet
105from lp.snappy.model.snapbuild import SnapBuild106from lp.snappy.model.snapbuild import SnapBuild
106from lp.soyuz.interfaces.archive import ArchiveDisabled107from lp.soyuz.interfaces.archive import ArchiveDisabled
107from lp.soyuz.model.archive import (108from lp.soyuz.model.archive import (
@@ -291,6 +292,18 @@
291 or not self.require_virtualized))]292 or not self.require_virtualized))]
292293
293 @property294 @property
295 def store_distro_series(self):
296 if self.store_series is None:
297 return None
298 return getUtility(ISnappyDistroSeriesSet).getByBothSeries(
299 self.store_series, self.distro_series)
300
301 @store_distro_series.setter
302 def store_distro_series(self, value):
303 self.distro_series = value.distro_series
304 self.store_series = value.snappy_series
305
306 @property
294 def can_upload_to_store(self):307 def can_upload_to_store(self):
295 return (308 return (
296 config.snappy.store_upload_url is not None and309 config.snappy.store_upload_url is not None and
297310
=== modified file 'lib/lp/snappy/model/snappyseries.py'
--- lib/lp/snappy/model/snappyseries.py 2016-05-09 13:24:10 +0000
+++ lib/lp/snappy/model/snappyseries.py 2016-05-27 10:39:59 +0000
@@ -182,3 +182,7 @@
182 SnappyDistroSeries,182 SnappyDistroSeries,
183 SnappyDistroSeries.snappy_series == snappy_series,183 SnappyDistroSeries.snappy_series == snappy_series,
184 SnappyDistroSeries.distro_series == distro_series).one()184 SnappyDistroSeries.distro_series == distro_series).one()
185
186 def getAll(self):
187 """See `ISnappyDistroSeriesSet`."""
188 return IStore(SnappyDistroSeries).find(SnappyDistroSeries)
185189
=== modified file 'lib/lp/snappy/templates/snap-edit.pt'
--- lib/lp/snappy/templates/snap-edit.pt 2016-02-04 00:45:12 +0000
+++ lib/lp/snappy/templates/snap-edit.pt 2016-05-27 10:39:59 +0000
@@ -25,10 +25,28 @@
25 <tal:widget define="widget nocall:view/widgets/name">25 <tal:widget define="widget nocall:view/widgets/name">
26 <metal:block use-macro="context/@@launchpad_form/widget_row" />26 <metal:block use-macro="context/@@launchpad_form/widget_row" />
27 </tal:widget>27 </tal:widget>
28 <tal:widget define="widget nocall:view/widgets/distro_series">28 <tal:widget define="widget nocall:view/widgets/store_distro_series">
29 <metal:block use-macro="context/@@launchpad_form/widget_row" />29 <metal:block use-macro="context/@@launchpad_form/widget_row" />
30 </tal:widget>30 </tal:widget>
3131
32 <tr tal:condition="view/has_snappy_distro_series">
33 <td>
34 <tal:widget define="widget nocall:view/widgets/store_upload">
35 <metal:block use-macro="context/@@launchpad_form/widget_row" />
36 </tal:widget>
37 <table class="subordinate">
38 <tal:widget define="widget nocall:view/widgets/store_name">
39 <metal:block use-macro="context/@@launchpad_form/widget_row" />
40 </tal:widget>
41 </table>
42 <p class="formHelp">
43 If you change any settings related to automatically uploading
44 builds of this snap to the store, then the login service will
45 prompt you to authorize this request.
46 </p>
47 </td>
48 </tr>
49
32 <tr>50 <tr>
33 <td>51 <td>
34 <div>52 <div>
3553
=== modified file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt 2016-05-14 00:25:07 +0000
+++ lib/lp/snappy/templates/snap-index.pt 2016-05-27 10:39:59 +0000
@@ -63,6 +63,28 @@
63 </dl>63 </dl>
64 </div>64 </div>
6565
66 <div id="store_upload" class="two-column-list"
67 tal:condition="context/store_upload">
68 <dl id="store_series">
69 <dt>Store series:</dt>
70 <dd>
71 <a tal:replace="structure context/store_series/fmt:link"/>
72 <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
73 </dd>
74 </dl>
75 <dl id="store_name">
76 <dt>Registered store package name:</dt>
77 <dd>
78 <span tal:content="context/store_name"/>
79 <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
80 </dd>
81 </dl>
82 </div>
83 <p id="store_upload" tal:condition="not: context/store_upload">
84 Builds of this snap package are not automatically uploaded to the store.
85 <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
86 </p>
87
66 <h2>Latest builds</h2>88 <h2>Latest builds</h2>
67 <table id="latest-builds-listing" class="listing"89 <table id="latest-builds-listing" class="listing"
68 style="margin-bottom: 1em;">90 style="margin-bottom: 1em;">
6991
=== modified file 'lib/lp/snappy/templates/snap-new.pt'
--- lib/lp/snappy/templates/snap-new.pt 2016-02-04 00:45:12 +0000
+++ lib/lp/snappy/templates/snap-new.pt 2016-05-27 10:39:59 +0000
@@ -28,9 +28,27 @@
28 <tal:widget define="widget nocall:view/widgets/owner">28 <tal:widget define="widget nocall:view/widgets/owner">
29 <metal:block use-macro="context/@@launchpad_form/widget_row" />29 <metal:block use-macro="context/@@launchpad_form/widget_row" />
30 </tal:widget>30 </tal:widget>
31 <tal:widget define="widget nocall:view/widgets/distro_series">31 <tal:widget define="widget nocall:view/widgets/store_distro_series">
32 <metal:block use-macro="context/@@launchpad_form/widget_row" />32 <metal:block use-macro="context/@@launchpad_form/widget_row" />
33 </tal:widget>33 </tal:widget>
34
35 <tr tal:condition="view/has_snappy_distro_series">
36 <td>
37 <tal:widget define="widget nocall:view/widgets/store_upload">
38 <metal:block use-macro="context/@@launchpad_form/widget_row" />
39 </tal:widget>
40 <table class="subordinate">
41 <tal:widget define="widget nocall:view/widgets/store_name">
42 <metal:block use-macro="context/@@launchpad_form/widget_row" />
43 </tal:widget>
44 </table>
45 <p class="formHelp">
46 If you ask Launchpad to automatically upload builds of this
47 snap to the store on your behalf, then the login service
48 will prompt you to authorize this request.
49 </p>
50 </td>
51 </tr>
34 </table>52 </table>
35 </metal:formbody>53 </metal:formbody>
36 </div>54 </div>
3755
=== modified file 'lib/lp/snappy/tests/test_snappyseries.py'
--- lib/lp/snappy/tests/test_snappyseries.py 2016-05-06 12:08:29 +0000
+++ lib/lp/snappy/tests/test_snappyseries.py 2016-05-27 10:39:59 +0000
@@ -277,3 +277,20 @@
277 self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[0], dses[1]))277 self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[0], dses[1]))
278 self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[0]))278 self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[0]))
279 self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[1]))279 self.assertIsNone(sds_set.getByBothSeries(snappy_serieses[1], dses[1]))
280
281 def test_getAll(self):
282 dses = [self.factory.makeDistroSeries() for _ in range(2)]
283 snappy_serieses = [self.factory.makeSnappySeries() for _ in range(2)]
284 snappy_serieses[0].usable_distro_series = dses
285 snappy_serieses[1].usable_distro_series = [dses[0]]
286 sds_set = getUtility(ISnappyDistroSeriesSet)
287 self.assertThat(
288 sds_set.getAll(),
289 MatchesSetwise(
290 MatchesStructure.byEquality(
291 snappy_series=snappy_serieses[0], distro_series=dses[0]),
292 MatchesStructure.byEquality(
293 snappy_series=snappy_serieses[0], distro_series=dses[1]),
294 MatchesStructure.byEquality(
295 snappy_series=snappy_serieses[1], distro_series=dses[0]),
296 ))