launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #20358
[Merge] lp:~cjwatson/launchpad/snap-store-add-edit-views into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-store-add-edit-views into lp:launchpad with lp:~cjwatson/launchpad/snap-authorize-view as a prerequisite.
Commit message:
Allow configuring automatic store upload in SnapAddView and SnapEditView.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1572605 in Launchpad itself: "Automatically upload snap builds to store"
https://bugs.launchpad.net/launchpad/+bug/1572605
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-store-add-edit-views/+merge/294524
Allow configuring automatic store upload in SnapAddView and SnapEditView.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-store-add-edit-views into lp:launchpad.
=== 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-12 15:04:04 +0000
@@ -853,6 +853,12 @@
name="fmt"
/>
<adapter
+ for="lp.snappy.interfaces.snappyseries.ISnappySeries"
+ provides="zope.traversing.interfaces.IPathAdapter"
+ factory="lp.app.browser.tales.SnappySeriesFormatterAPI"
+ name="fmt"
+ />
+ <adapter
for="lp.blueprints.interfaces.specification.ISpecification"
provides="zope.traversing.interfaces.IPathAdapter"
factory="lp.app.browser.tales.SpecificationFormatterAPI"
=== 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-12 15:04:04 +0000
@@ -1872,6 +1872,15 @@
'owner': self._context.owner.displayname}
+class SnappySeriesFormatterAPI(CustomizableFormatter):
+ """Adapter providing fmt support for ISnappySeries objects."""
+
+ _link_summary_template = _('%(title)s')
+
+ def _link_summary_values(self):
+ return {'title': self._context.title}
+
+
class SpecificationFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for Specification objects"""
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2016-05-12 15:04:04 +0000
+++ lib/lp/snappy/browser/snap.py 2016-05-12 15:04:04 +0000
@@ -89,7 +89,14 @@
SnapPrivateFeatureDisabled,
)
from lp.snappy.interfaces.snapbuild import ISnapBuildSet
-from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
+from lp.snappy.interfaces.snappyseries import (
+ ISnappyDistroSeriesSet,
+ ISnappySeriesSet,
+ )
+from lp.snappy.interfaces.snapstoreclient import (
+ BadRequestPackageUploadResponse,
+ ISnapStoreClient,
+ )
from lp.soyuz.browser.archive import EnableProcessorsMixin
from lp.soyuz.browser.build import get_build_by_id_str
from lp.soyuz.interfaces.archive import IArchive
@@ -298,9 +305,11 @@
'name',
'private',
'require_virtualized',
+ 'store_upload',
])
- distro_series = Choice(
- vocabulary='BuildableDistroSeries', title=u'Distribution series')
+ store_distro_series = Choice(
+ vocabulary='BuildableSnappyDistroSeries', required=True,
+ title=u'Series')
vcs = Choice(vocabulary=VCSType, required=True, title=u'VCS')
# Each of these is only required if vcs has an appropriate value. Later
@@ -308,6 +317,10 @@
branch = copy_field(ISnap['branch'], required=True)
git_ref = copy_field(ISnap['git_ref'], required=True)
+ # These are only required if store_upload is True. Later validation
+ # takes care of adjusting the required attribute.
+ store_name = copy_field(ISnap['store_name'], required=True)
+
class SnapAddView(LaunchpadFormView):
"""View for creating snap packages."""
@@ -315,8 +328,14 @@
page_title = label = 'Create a new snap package'
schema = ISnapEditSchema
- field_names = ['owner', 'name', 'distro_series']
- custom_widget('distro_series', LaunchpadRadioWidget)
+ field_names = [
+ 'owner',
+ 'name',
+ 'store_distro_series',
+ 'store_upload',
+ 'store_name',
+ ]
+ custom_widget('store_distro_series', LaunchpadRadioWidget)
def initialize(self):
"""See `LaunchpadView`."""
@@ -342,13 +361,28 @@
# accidentally selecting ubuntu-rtm/14.09 or similar.
# ubuntu.currentseries will always be in BuildableDistroSeries.
series = getUtility(ILaunchpadCelebrities).ubuntu.currentseries
+ sds_set = getUtility(ISnappyDistroSeriesSet)
return {
'owner': self.user,
- 'distro_series': series,
+ 'store_distro_series': sds_set.getByDistroSeries(series).first(),
}
+ @property
+ def has_snappy_series(self):
+ return not getUtility(ISnappySeriesSet).getAll().is_empty()
+
+ def validate_widgets(self, data, names=None):
+ """See `LaunchpadFormView`."""
+ if self.widgets.get('store_upload') is not None:
+ # Set widgets as required or optional depending on the
+ # store_upload field.
+ super(SnapAddView, self).validate_widgets(data, ['store_upload'])
+ store_upload = data.get('store_upload', False)
+ self.widgets['store_name'].context.required = store_upload
+ super(SnapAddView, self).validate_widgets(data, names=names)
+
@action('Create snap package', name='create')
- def request_action(self, action, data):
+ def create_action(self, action, data):
if IGitRef.providedBy(self.context):
kwargs = {'git_ref': self.context}
else:
@@ -356,9 +390,20 @@
private = not getUtility(
ISnapSet).isValidPrivacy(False, data['owner'], **kwargs)
snap = getUtility(ISnapSet).new(
- self.user, data['owner'], data['distro_series'], data['name'],
- private=private, **kwargs)
- self.next_url = canonical_url(snap)
+ self.user, data['owner'],
+ data['store_distro_series'].distro_series, data['name'],
+ private=private, store_upload=data['store_upload'],
+ store_series=data['store_distro_series'].snappy_series,
+ store_name=data['store_name'], **kwargs)
+ if data['store_upload']:
+ try:
+ self.next_url = SnapAuthorizeView.requestAuthorization(
+ snap, self.request)
+ except BadRequestPackageUploadResponse as e:
+ self.setFieldError(
+ 'store_upload', 'Cannot upload this package: %s' % e)
+ else:
+ self.next_url = canonical_url(snap)
def validate(self, data):
super(SnapAddView, self).validate(data)
@@ -390,6 +435,10 @@
render_radio_widget_part(widget, value, current_value)
for value in (VCSType.BZR, VCSType.GIT)]
+ @property
+ def has_snappy_series(self):
+ return not getUtility(ISnappySeriesSet).getAll().is_empty()
+
def validate_widgets(self, data, names=None):
"""See `LaunchpadFormView`."""
if self.widgets.get('vcs') is not None:
@@ -405,8 +454,29 @@
self.widgets['git_ref'].context.required = True
else:
raise AssertionError("Unknown branch type %s" % vcs)
+ if self.widgets.get('store_upload') is not None:
+ # Set widgets as required or optional depending on the
+ # store_upload field.
+ super(BaseSnapEditView, self).validate_widgets(
+ data, ['store_upload'])
+ store_upload = data.get('store_upload', False)
+ self.widgets['store_name'].context.required = store_upload
super(BaseSnapEditView, self).validate_widgets(data, names=names)
+ def _needStoreReauth(self, data):
+ """Does this change require reauthorizing to the store?"""
+ store_upload = data.get('store_upload', False)
+ store_distro_series = data.get('store_distro_series')
+ store_name = data.get('store_name')
+ if (not store_upload or
+ store_distro_series is None or store_name is None):
+ return False
+ if store_distro_series.snappy_series != self.context.store_series:
+ return True
+ if store_name != self.context.store_name:
+ return True
+ return False
+
@action('Update snap package', name='update')
def request_action(self, action, data):
vcs = data.pop('vcs', None)
@@ -420,8 +490,20 @@
self.context.setProcessors(
new_processors, check_permissions=True, user=self.user)
del data['processors']
+ store_upload = data.get('store_upload', False)
+ if not store_upload:
+ data['store_name'] = None
+ need_store_reauth = self._needStoreReauth(data)
self.updateContextFromData(data)
- self.next_url = canonical_url(self.context)
+ if need_store_reauth:
+ try:
+ self.next_url = SnapAuthorizeView.requestAuthorization(
+ self.context, self.request)
+ except BadRequestPackageUploadResponse as e:
+ self.setFieldError(
+ 'store_upload', 'Cannot upload this package: %s' % e)
+ else:
+ self.next_url = canonical_url(self.context)
@property
def adapters(self):
@@ -464,8 +546,16 @@
page_title = 'Edit'
field_names = [
- 'owner', 'name', 'distro_series', 'vcs', 'branch', 'git_ref']
- custom_widget('distro_series', LaunchpadRadioWidget)
+ 'owner',
+ 'name',
+ 'store_distro_series',
+ 'store_upload',
+ 'store_name',
+ 'vcs',
+ 'branch',
+ 'git_ref',
+ ]
+ custom_widget('store_distro_series', LaunchpadRadioWidget)
custom_widget('vcs', LaunchpadRadioWidget)
custom_widget('git_ref', GitRefWidget)
@@ -480,11 +570,18 @@
@property
def initial_values(self):
+ initial_values = {}
+ if self.context.store_series is None:
+ # XXX cjwatson 2016-04-26: Remove this case once all existing
+ # Snaps have had a store_series backfilled.
+ sds_set = getUtility(ISnappyDistroSeriesSet)
+ initial_values['store_distro_series'] = sds_set.getByDistroSeries(
+ self.context.distro_series).first()
if self.context.git_ref is not None:
- vcs = VCSType.GIT
+ initial_values['vcs'] = VCSType.GIT
else:
- vcs = VCSType.BZR
- return {'vcs': vcs}
+ initial_values['vcs'] = VCSType.BZR
+ return initial_values
def validate(self, data):
super(SnapEditView, self).validate(data)
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2016-05-12 15:04:04 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2016-05-12 15:04:04 +0000
@@ -12,6 +12,7 @@
import json
import re
from textwrap import dedent
+from urllib2 import HTTPError
from urlparse import urlsplit
from fixtures import FakeLogger
@@ -53,6 +54,7 @@
)
from lp.snappy.interfaces.snap import (
CannotModifySnapProcessor,
+ ISnapSet,
SNAP_FEATURE_FLAG,
SNAP_TESTING_FLAGS,
SnapFeatureDisabled,
@@ -138,7 +140,7 @@
class TestSnapAddView(BrowserTestCase):
- layer = DatabaseFunctionalLayer
+ layer = LaunchpadFunctionalLayer
def setUp(self):
super(TestSnapAddView, self).setUp()
@@ -146,24 +148,35 @@
self.useFixture(FakeLogger())
self.person = self.factory.makePerson(
name="test-person", displayname="Test Person")
+ self.distroseries = self.factory.makeUbuntuDistroSeries(
+ version="13.10")
+ with admin_logged_in():
+ self.snappyseries = self.factory.makeSnappySeries(
+ usable_distro_series=[self.distroseries])
def test_initial_distroseries(self):
# The initial distroseries is the newest that is current or in
# development.
- archive = self.factory.makeArchive(owner=self.person)
- self.factory.makeDistroSeries(
- distribution=archive.distribution, version="14.04",
- status=SeriesStatus.DEVELOPMENT)
- development = self.factory.makeDistroSeries(
- distribution=archive.distribution, version="14.10",
- status=SeriesStatus.DEVELOPMENT)
- self.factory.makeDistroSeries(
- distribution=archive.distribution, version="15.04",
- status=SeriesStatus.EXPERIMENTAL)
+ old = self.factory.makeUbuntuDistroSeries(
+ version="14.04", status=SeriesStatus.DEVELOPMENT)
+ development = self.factory.makeUbuntuDistroSeries(
+ version="14.10", status=SeriesStatus.DEVELOPMENT)
+ experimental = self.factory.makeUbuntuDistroSeries(
+ version="15.04", status=SeriesStatus.EXPERIMENTAL)
+ with admin_logged_in():
+ self.factory.makeSnappySeries(
+ usable_distro_series=[old, development, experimental])
+ newest = self.factory.makeSnappySeries(
+ usable_distro_series=[development, experimental])
+ self.factory.makeSnappySeries(
+ usable_distro_series=[old, experimental])
branch = self.factory.makeAnyBranch()
with person_logged_in(self.person):
view = create_initialized_view(branch, "+new-snap")
- self.assertEqual(development, view.initial_values["distro_series"])
+ self.assertThat(
+ view.initial_values["store_distro_series"],
+ MatchesStructure.byEquality(
+ snappy_series=newest, distro_series=development))
def test_create_new_snap_not_logged_in(self):
branch = self.factory.makeAnyBranch()
@@ -172,14 +185,11 @@
no_login=True)
def test_create_new_snap_bzr(self):
- archive = self.factory.makeArchive()
- distroseries = self.factory.makeDistroSeries(
- distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)
branch = self.factory.makeAnyBranch()
source_display = branch.display_name
browser = self.getViewBrowser(
branch, view_name="+new-snap", user=self.person)
- browser.getControl("Name").value = "snap-name"
+ browser.getControl(name="field.name").value = "snap-name"
browser.getControl("Create snap package").click()
content = find_main_content(browser.contents)
@@ -188,21 +198,22 @@
"Test Person", MatchesPickerText(content, "edit-owner"))
self.assertThat(
"Distribution series:\n%s\nEdit snap package" %
- distroseries.fullseriesname,
+ self.distroseries.fullseriesname,
MatchesTagText(content, "distro_series"))
self.assertThat(
"Source:\n%s\nEdit snap package" % source_display,
MatchesTagText(content, "source"))
+ self.assertThat(
+ "Builds of this snap package are not automatically uploaded to "
+ "the store.\nEdit snap package",
+ MatchesTagText(content, "store_upload"))
def test_create_new_snap_git(self):
- archive = self.factory.makeArchive()
- distroseries = self.factory.makeDistroSeries(
- distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)
[git_ref] = self.factory.makeGitRefs()
source_display = git_ref.display_name
browser = self.getViewBrowser(
git_ref, view_name="+new-snap", user=self.person)
- browser.getControl("Name").value = "snap-name"
+ browser.getControl(name="field.name").value = "snap-name"
browser.getControl("Create snap package").click()
content = find_main_content(browser.contents)
@@ -211,11 +222,15 @@
"Test Person", MatchesPickerText(content, "edit-owner"))
self.assertThat(
"Distribution series:\n%s\nEdit snap package" %
- distroseries.fullseriesname,
+ self.distroseries.fullseriesname,
MatchesTagText(content, "distro_series"))
self.assertThat(
"Source:\n%s\nEdit snap package" % source_display,
MatchesTagText(content, "source"))
+ self.assertThat(
+ "Builds of this snap package are not automatically uploaded to "
+ "the store.\nEdit snap package",
+ MatchesTagText(content, "store_upload"))
def test_create_new_snap_users_teams_as_owner_options(self):
# Teams that the user is in are options for the snap package owner.
@@ -230,12 +245,12 @@
sorted(str(option) for option in options))
def test_create_new_snap_public(self):
- # Public owner implies in public snap.
+ # Public owner implies public snap.
branch = self.factory.makeAnyBranch()
browser = self.getViewBrowser(
branch, view_name="+new-snap", user=self.person)
- browser.getControl("Name").value = "public-snap"
+ browser.getControl(name="field.name").value = "public-snap"
browser.getControl("Create snap package").click()
content = find_main_content(browser.contents)
@@ -271,7 +286,7 @@
browser = self.getViewBrowser(
branch, view_name="+new-snap", user=self.person)
- browser.getControl("Name").value = "private-snap"
+ browser.getControl(name="field.name").value = "private-snap"
browser.getControl("Owner").value = ['super-private']
browser.getControl("Create snap package").click()
@@ -282,6 +297,56 @@
extract_text(find_tag_by_id(browser.contents, "privacy"))
)
+ def test_create_new_snap_store_upload(self):
+ # Creating a new snap and asking for it to be automatically uploaded
+ # to the store sets all the appropriate fields and redirects to SSO
+ # for authorization.
+ branch = self.factory.makeAnyBranch()
+ view_url = canonical_url(branch, view_name="+new-snap")
+ browser = self.getNonRedirectingBrowser(url=view_url, user=self.person)
+ browser.getControl(name="field.name").value = "snap-name"
+ browser.getControl("Automatically upload to store").selected = True
+ browser.getControl("Registered store package name").value = (
+ "store-name")
+ root_macaroon = Macaroon()
+ root_macaroon.add_third_party_caveat(
+ urlsplit(config.launchpad.openid_provider_root).netloc, "",
+ "dummy")
+ root_macaroon_raw = root_macaroon.serialize()
+
+ @all_requests
+ def handler(url, request):
+ self.request = request
+ return {
+ "status_code": 200,
+ "content": {"macaroon": root_macaroon_raw},
+ }
+
+ self.pushConfig("snappy", store_url="http://sca.example/")
+ with HTTMock(handler):
+ redirection = self.assertRaises(
+ HTTPError, browser.getControl("Create snap package").click)
+ login_person(self.person)
+ snap = getUtility(ISnapSet).getByName(self.person, u"snap-name")
+ self.assertThat(snap, MatchesStructure.byEquality(
+ owner=self.person, distro_series=self.distroseries,
+ name=u"snap-name", source=branch, store_upload=True,
+ store_series=self.snappyseries, store_name=u"store-name",
+ store_secrets={"root": root_macaroon_raw}))
+ self.assertThat(self.request, MatchesStructure.byEquality(
+ url="http://sca.example/api/2.0/acl/package_upload/",
+ method="POST"))
+ self.assertEqual(
+ {"name": "store-name", "series": self.snappyseries.name},
+ json.loads(self.request.body))
+ self.assertEqual(303, redirection.code)
+ self.assertEqual(
+ canonical_url(snap, rootsite="code") +
+ "/+authorize/+login?field.callback=on&"
+ "macaroon_caveat_id=dummy&"
+ "discharge_macaroon_field=field.discharge_macaroon",
+ redirection.hdrs["Location"])
+
class TestSnapAdminView(BrowserTestCase):
@@ -371,27 +436,53 @@
self.useFixture(FakeLogger())
self.person = self.factory.makePerson(
name="test-person", displayname="Test Person")
+ self.distroseries = self.factory.makeUbuntuDistroSeries(
+ version="13.10")
+ with admin_logged_in():
+ self.snappyseries = self.factory.makeSnappySeries(
+ usable_distro_series=[self.distroseries])
+
+ def test_initial_store_series(self):
+ # The initial store_series is the newest that is usable for the
+ # selected distroseries.
+ development = self.factory.makeUbuntuDistroSeries(
+ version="14.10", status=SeriesStatus.DEVELOPMENT)
+ experimental = self.factory.makeUbuntuDistroSeries(
+ version="15.04", status=SeriesStatus.EXPERIMENTAL)
+ with admin_logged_in():
+ self.factory.makeSnappySeries(
+ usable_distro_series=[development, experimental])
+ newest = self.factory.makeSnappySeries(
+ usable_distro_series=[development])
+ self.factory.makeSnappySeries(usable_distro_series=[experimental])
+ snap = self.factory.makeSnap(distroseries=development)
+ with person_logged_in(self.person):
+ view = create_initialized_view(snap, "+edit")
+ self.assertThat(
+ view.initial_values["store_distro_series"],
+ MatchesStructure.byEquality(
+ snappy_series=newest, distro_series=development))
def test_edit_snap(self):
- archive = self.factory.makeArchive()
- old_series = self.factory.makeDistroSeries(
- distribution=archive.distribution, status=SeriesStatus.CURRENT)
+ old_series = self.factory.makeUbuntuDistroSeries()
old_branch = self.factory.makeAnyBranch()
snap = self.factory.makeSnap(
registrant=self.person, owner=self.person, distroseries=old_series,
branch=old_branch)
self.factory.makeTeam(
name="new-team", displayname="New Team", members=[self.person])
- new_series = self.factory.makeDistroSeries(
- distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)
+ new_series = self.factory.makeUbuntuDistroSeries()
+ with admin_logged_in():
+ new_snappy_series = self.factory.makeSnappySeries(
+ usable_distro_series=[new_series])
[new_git_ref] = self.factory.makeGitRefs()
browser = self.getViewBrowser(snap, user=self.person)
browser.getLink("Edit snap package").click()
browser.getControl("Owner").value = ["new-team"]
- browser.getControl("Name").value = "new-name"
- browser.getControl(name="field.distro_series").value = [
- str(new_series.id)]
+ browser.getControl(name="field.name").value = "new-name"
+ browser.getControl(name="field.store_distro_series").value = [
+ "ubuntu/%s/%s" % (new_series.name, new_snappy_series.name)]
browser.getControl("Git", index=0).click()
browser.getControl("Git repository").value = (
new_git_ref.repository.identity)
@@ -408,6 +499,10 @@
self.assertThat(
"Source:\n%s\nEdit snap package" % new_git_ref.display_name,
MatchesTagText(content, "source"))
+ self.assertThat(
+ "Builds of this snap package are not automatically uploaded to "
+ "the store.\nEdit snap package",
+ MatchesTagText(content, "store_upload"))
def test_edit_snap_sets_date_last_modified(self):
# Editing a snap package sets the date_last_modified property.
@@ -431,7 +526,7 @@
registrant=self.person, owner=self.person, name=u"two")
browser = self.getViewBrowser(snap, user=self.person)
browser.getLink("Edit snap package").click()
- browser.getControl("Name").value = "two"
+ browser.getControl(name="field.name").value = "two"
browser.getControl("Update snap package").click()
self.assertEqual(
"There is already a snap package owned by Test Person with this "
@@ -447,6 +542,8 @@
self.factory.makeDistroArchSeries(
distroseries=distroseries, architecturetag=name,
processor=processor)
+ with admin_logged_in():
+ self.factory.makeSnappySeries(usable_distro_series=[distroseries])
return distroseries
def assertSnapProcessors(self, snap, names):
@@ -570,6 +667,51 @@
login_person(self.person)
self.assertSnapProcessors(snap, ["386", "armhf"])
+ def test_edit_store_upload(self):
+ # Changing store upload settings on a snap sets all the appropriate
+ # fields and redirects to SSO for reauthorization.
+ snap = self.factory.makeSnap(
+ registrant=self.person, owner=self.person,
+ distroseries=self.distroseries, store_upload=True,
+ store_series=self.snappyseries, store_name=u"one")
+ view_url = canonical_url(snap, view_name="+edit")
+ browser = self.getNonRedirectingBrowser(url=view_url, user=self.person)
+ browser.getControl("Registered store package name").value = "two"
+ root_macaroon = Macaroon()
+ root_macaroon.add_third_party_caveat(
+ urlsplit(config.launchpad.openid_provider_root).netloc, "",
+ "dummy")
+ root_macaroon_raw = root_macaroon.serialize()
+
+ @all_requests
+ def handler(url, request):
+ self.request = request
+ return {
+ "status_code": 200,
+ "content": {"macaroon": root_macaroon_raw},
+ }
+
+ self.pushConfig("snappy", store_url="http://sca.example/")
+ with HTTMock(handler):
+ redirection = self.assertRaises(
+ HTTPError, browser.getControl("Update snap package").click)
+ login_person(self.person)
+ self.assertThat(snap, MatchesStructure.byEquality(
+ store_name=u"two", store_secrets={"root": root_macaroon_raw}))
+ self.assertThat(self.request, MatchesStructure.byEquality(
+ url="http://sca.example/api/2.0/acl/package_upload/",
+ method="POST"))
+ self.assertEqual(
+ {"name": "two", "series": self.snappyseries.name},
+ json.loads(self.request.body))
+ self.assertEqual(303, redirection.code)
+ self.assertEqual(
+ canonical_url(snap) +
+ "/+authorize/+login?field.callback=on&"
+ "macaroon_caveat_id=dummy&"
+ "discharge_macaroon_field=field.discharge_macaroon",
+ redirection.hdrs["Location"])
+
class TestSnapAuthorizeView(BrowserTestCase):
@@ -846,6 +988,8 @@
Owner: Test Person
Distribution series: Ubuntu Shiny
Source: lp://dev/~test-person/\\+junk/snap-branch
+ Builds of this snap package are not automatically uploaded to
+ the store.
Latest builds
Status When complete Architecture Archive
Successfully built 30 minutes ago i386
@@ -867,6 +1011,8 @@
Owner: Test Person
Distribution series: Ubuntu Shiny
Source: ~test-person/\\+git/snap-repository:master
+ Builds of this snap package are not automatically uploaded to
+ the store.
Latest builds
Status When complete Architecture Archive
Successfully built 30 minutes ago i386
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2016-05-11 00:00:47 +0000
+++ lib/lp/snappy/interfaces/snap.py 2016-05-12 15:04:04 +0000
@@ -86,7 +86,10 @@
PublicPersonChoice,
)
from lp.services.webhooks.interfaces import IWebhookTarget
-from lp.snappy.interfaces.snappyseries import ISnappySeries
+from lp.snappy.interfaces.snappyseries import (
+ ISnappyDistroSeries,
+ ISnappySeries,
+ )
from lp.soyuz.interfaces.archive import IArchive
from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
@@ -374,6 +377,11 @@
"The series in which this snap package should be published in the "
"store."))
+ store_distro_series = ReferenceChoice(
+ title=_("Store and distro series"),
+ schema=ISnappyDistroSeries, vocabulary="SnappyDistroSeries",
+ required=False, readonly=False)
+
store_name = TextLine(
title=_("Registered store package name"),
required=False, readonly=False,
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2016-05-11 00:00:47 +0000
+++ lib/lp/snappy/model/snap.py 2016-05-12 15:04:04 +0000
@@ -94,6 +94,7 @@
SnapPrivateFeatureDisabled,
)
from lp.snappy.interfaces.snapbuild import ISnapBuildSet
+from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet
from lp.snappy.model.snapbuild import SnapBuild
from lp.soyuz.interfaces.archive import ArchiveDisabled
from lp.soyuz.model.archive import (
@@ -283,6 +284,18 @@
or not self.require_virtualized))]
@property
+ def store_distro_series(self):
+ if self.store_series is None:
+ return None
+ return getUtility(ISnappyDistroSeriesSet).getByBothSeries(
+ self.store_series, self.distro_series)
+
+ @store_distro_series.setter
+ def store_distro_series(self, value):
+ self.distro_series = value.distro_series
+ self.store_series = value.snappy_series
+
+ @property
def can_upload_to_store(self):
return (
config.snappy.store_upload_url is not None and
=== 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-12 15:04:04 +0000
@@ -25,10 +25,28 @@
<tal:widget define="widget nocall:view/widgets/name">
<metal:block use-macro="context/@@launchpad_form/widget_row" />
</tal:widget>
- <tal:widget define="widget nocall:view/widgets/distro_series">
+ <tal:widget define="widget nocall:view/widgets/store_distro_series">
<metal:block use-macro="context/@@launchpad_form/widget_row" />
</tal:widget>
+ <tr tal:condition="view/has_snappy_series">
+ <td>
+ <tal:widget define="widget nocall:view/widgets/store_upload">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <table class="subordinate">
+ <tal:widget define="widget nocall:view/widgets/store_name">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ </table>
+ <p class="formHelp">
+ If you change any settings related to automatically uploading
+ builds of this snap to the store, then the login service will
+ prompt you to authorize this request.
+ </p>
+ </td>
+ </tr>
+
<tr>
<td>
<div>
=== modified file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt 2016-01-27 12:43:00 +0000
+++ lib/lp/snappy/templates/snap-index.pt 2016-05-12 15:04:04 +0000
@@ -51,6 +51,28 @@
</dl>
</div>
+ <div id="store_upload" class="two-column-list"
+ tal:condition="context/store_upload">
+ <dl id="store_series">
+ <dt>Store series:</dt>
+ <dd>
+ <a tal:replace="structure context/store_series/fmt:link"/>
+ <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
+ </dd>
+ </dl>
+ <dl id="store_name">
+ <dt>Registered store package name:</dt>
+ <dd>
+ <span tal:content="context/store_name"/>
+ <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
+ </dd>
+ </dl>
+ </div>
+ <p id="store_upload" tal:condition="not: context/store_upload">
+ Builds of this snap package are not automatically uploaded to the store.
+ <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
+ </p>
+
<h2>Latest builds</h2>
<table id="latest-builds-listing" class="listing"
style="margin-bottom: 1em;">
=== 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-12 15:04:04 +0000
@@ -28,9 +28,27 @@
<tal:widget define="widget nocall:view/widgets/owner">
<metal:block use-macro="context/@@launchpad_form/widget_row" />
</tal:widget>
- <tal:widget define="widget nocall:view/widgets/distro_series">
+ <tal:widget define="widget nocall:view/widgets/store_distro_series">
<metal:block use-macro="context/@@launchpad_form/widget_row" />
</tal:widget>
+
+ <tr tal:condition="view/has_snappy_series">
+ <td>
+ <tal:widget define="widget nocall:view/widgets/store_upload">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <table class="subordinate">
+ <tal:widget define="widget nocall:view/widgets/store_name">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ </table>
+ <p class="formHelp">
+ If you ask Launchpad to automatically upload builds of this
+ snap to the store on your behalf, then the login service
+ will prompt you to authorize this request.
+ </p>
+ </td>
+ </tr>
</table>
</metal:formbody>
</div>
Follow ups