launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19379
[Merge] lp:~cjwatson/launchpad/snap-add-view into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-add-view into lp:launchpad with lp:~cjwatson/launchpad/snap-listings as a prerequisite.
Commit message:
Add views for creating snap packages based on Bazaar or Git branches.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1476405 in Launchpad itself: "Add support for building snaps"
https://bugs.launchpad.net/launchpad/+bug/1476405
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-add-view/+merge/271360
Add views for creating snap packages based on Bazaar or Git branches.
GitRepository:+index now has a "View snap packages" link but no way to create one directly; you have to go through a ref, which might be slightly non-obvious. I did this because it simplified SnapAddView to not have to worry about the source at all for now and just take it all from the context. This could be improved in future if need be.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-add-view into lp:launchpad.
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2015-09-16 19:19:41 +0000
+++ lib/lp/code/browser/branch.py 2015-09-16 19:19:41 +0000
@@ -128,6 +128,7 @@
from lp.services.config import config
from lp.services.database.bulk import load_related
from lp.services.database.constants import UTC_NOW
+from lp.services.features import getFeatureFlag
from lp.services.feeds.browser import (
BranchFeedLink,
FeedsMixin,
@@ -159,6 +160,7 @@
HasSnapsMenuMixin,
HasSnapsViewMixin,
)
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
from lp.translations.interfaces.translationtemplatesbuild import (
ITranslationTemplatesBuildSource,
)
@@ -277,10 +279,10 @@
usedfor = IBranch
facet = 'branches'
links = [
- 'add_subscriber', 'browse_revisions', 'create_recipe', 'link_bug',
- 'link_blueprint', 'register_merge', 'source', 'subscription',
- 'edit_status', 'edit_import', 'upgrade_branch', 'view_recipes',
- 'view_snaps', 'visibility']
+ 'add_subscriber', 'browse_revisions', 'create_recipe', 'create_snap',
+ 'link_bug', 'link_blueprint', 'register_merge', 'source',
+ 'subscription', 'edit_status', 'edit_import', 'upgrade_branch',
+ 'view_recipes', 'view_snaps', 'visibility']
@enabled_with_permission('launchpad.Edit')
def edit_status(self):
@@ -368,6 +370,14 @@
text = 'Create packaging recipe'
return Link('+new-recipe', text, enabled=enabled, icon='add')
+ def create_snap(self):
+ # You can't yet create a snap for a private branch.
+ enabled = (
+ bool(getFeatureFlag(SNAP_FEATURE_FLAG)) and
+ not self.context.private)
+ text = 'Create snap package'
+ return Link('+new-snap', text, enabled=enabled, icon='add')
+
class BranchMirrorMixin:
"""Provide mirror_location property.
=== modified file 'lib/lp/code/browser/gitref.py'
--- lib/lp/code/browser/gitref.py 2015-09-16 19:19:41 +0000
+++ lib/lp/code/browser/gitref.py 2015-09-16 19:19:41 +0000
@@ -43,6 +43,7 @@
from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.code.model.gitrepository import GitRepository
from lp.services.database.bulk import load_related
+from lp.services.features import getFeatureFlag
from lp.services.helpers import english_list
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
@@ -55,6 +56,7 @@
)
from lp.services.webapp.authorization import check_permission
from lp.snappy.browser.hassnaps import HasSnapsViewMixin
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
# XXX cjwatson 2015-05-26: We can get rid of this after a short while, since
@@ -81,13 +83,21 @@
usedfor = IGitRef
facet = 'branches'
- links = ['register_merge']
+ links = ['create_snap', 'register_merge']
def register_merge(self):
text = 'Propose for merging'
enabled = self.context.namespace.supports_merge_proposals
return Link('+register-merge', text, icon='add', enabled=enabled)
+ def create_snap(self):
+ # You can't yet create a snap for a private repository.
+ enabled = (
+ bool(getFeatureFlag(SNAP_FEATURE_FLAG)) and
+ not self.context.private)
+ text = "Create snap package"
+ return Link("+new-snap", text, enabled=enabled, icon="add")
+
class GitRefView(LaunchpadView, HasSnapsViewMixin):
=== modified file 'lib/lp/code/browser/tests/test_branch.py'
--- lib/lp/code/browser/tests/test_branch.py 2015-09-10 08:47:06 +0000
+++ lib/lp/code/browser/tests/test_branch.py 2015-09-16 19:19:41 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Unit tests for BranchView."""
@@ -560,7 +560,7 @@
logout()
with StormStatementRecorder() as recorder:
browser.open(branch_url)
- self.assertThat(recorder, HasQueryCount(Equals(28)))
+ self.assertThat(recorder, HasQueryCount(Equals(29)))
class TestBranchViewPrivateArtifacts(BrowserTestCase):
=== modified file 'lib/lp/code/templates/branch-snaps.pt'
--- lib/lp/code/templates/branch-snaps.pt 2015-09-16 19:19:41 +0000
+++ lib/lp/code/templates/branch-snaps.pt 2015-09-16 19:19:41 +0000
@@ -19,6 +19,12 @@
</tal:snaps>
using this branch.
</div>
+
+ <span
+ tal:define="link context_menu/create_snap"
+ tal:condition="link/enabled"
+ tal:replace="structure link/render"
+ />
</div>
</div>
=== modified file 'lib/lp/code/templates/gitref-snaps.pt'
--- lib/lp/code/templates/gitref-snaps.pt 2015-09-16 19:19:41 +0000
+++ lib/lp/code/templates/gitref-snaps.pt 2015-09-16 19:19:41 +0000
@@ -20,6 +20,12 @@
</tal:snaps>
using this branch.
</div>
+
+ <span
+ tal:define="link context_menu/create_snap"
+ tal:condition="link/enabled"
+ tal:replace="structure link/render"
+ />
</div>
</div>
=== modified file 'lib/lp/snappy/browser/configure.zcml'
--- lib/lp/snappy/browser/configure.zcml 2015-09-16 19:19:41 +0000
+++ lib/lp/snappy/browser/configure.zcml 2015-09-16 19:19:41 +0000
@@ -29,6 +29,18 @@
module="lp.snappy.browser.snap"
classes="SnapNavigation" />
<browser:page
+ for="lp.code.interfaces.branch.IBranch"
+ class="lp.snappy.browser.snap.SnapAddView"
+ permission="launchpad.AnyPerson"
+ name="+new-snap"
+ template="../templates/snap-new.pt" />
+ <browser:page
+ for="lp.code.interfaces.gitref.IGitRef"
+ class="lp.snappy.browser.snap.SnapAddView"
+ permission="launchpad.AnyPerson"
+ name="+new-snap"
+ template="../templates/snap-new.pt" />
+ <browser:page
for="lp.snappy.interfaces.snap.ISnap"
class="lp.snappy.browser.snap.SnapAdminView"
permission="launchpad.Admin"
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2015-09-16 19:19:41 +0000
+++ lib/lp/snappy/browser/snap.py 2015-09-16 19:19:41 +0000
@@ -5,6 +5,7 @@
__metaclass__ = type
__all__ = [
+ 'SnapAddView',
'SnapDeleteView',
'SnapEditView',
'SnapNavigation',
@@ -24,13 +25,18 @@
action,
custom_widget,
LaunchpadEditFormView,
+ LaunchpadFormView,
render_radio_widget_part,
)
from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
from lp.app.browser.tales import format_link
from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
from lp.code.browser.widgets.gitref import GitRefWidget
+from lp.code.interfaces.gitref import IGitRef
+from lp.code.vocabularies.sourcepackagerecipe import BuildableDistroSeries
from lp.registry.enums import VCSType
+from lp.registry.interfaces.series import SeriesStatus
+from lp.services.features import getFeatureFlag
from lp.services.webapp import (
canonical_url,
enabled_with_permission,
@@ -48,6 +54,8 @@
from lp.snappy.interfaces.snap import (
ISnap,
ISnapSet,
+ SNAP_FEATURE_FLAG,
+ SnapFeatureDisabled,
NoSuchSnap,
)
from lp.snappy.interfaces.snapbuild import ISnapBuildSet
@@ -156,7 +164,60 @@
git_ref = copy_field(ISnap['git_ref'], required=True)
-class BaseSnapAddEditView(LaunchpadEditFormView):
+class SnapAddView(LaunchpadFormView):
+ """View for creating snap packages."""
+
+ page_title = label = 'Create a new snap package'
+
+ schema = ISnapEditSchema
+ field_names = ['owner', 'name', 'distro_series']
+ custom_widget('distro_series', LaunchpadRadioWidget)
+
+ def initialize(self):
+ """See `LaunchpadView`."""
+ if not getFeatureFlag(SNAP_FEATURE_FLAG):
+ raise SnapFeatureDisabled
+ super(SnapAddView, self).initialize()
+
+ @property
+ def cancel_url(self):
+ return canonical_url(self.context)
+
+ @property
+ def initial_values(self):
+ series = [
+ term.value for term in BuildableDistroSeries()
+ if term.value.status in (
+ SeriesStatus.CURRENT, SeriesStatus.DEVELOPMENT)][0]
+ return {
+ 'owner': self.user,
+ 'distro_series': series,
+ }
+
+ @action('Create snap package', name='create')
+ def request_action(self, action, data):
+ if IGitRef.providedBy(self.context):
+ kwargs = {'git_ref': self.context}
+ else:
+ kwargs = {'branch': self.context}
+ snap = getUtility(ISnapSet).new(
+ self.user, data['owner'], data['distro_series'], data['name'],
+ **kwargs)
+ self.next_url = canonical_url(snap)
+
+ def validate(self, data):
+ super(SnapAddView, self).validate(data)
+ owner = data.get('owner', None)
+ name = data.get('name', None)
+ if owner and name:
+ if getUtility(ISnapSet).exists(owner, name):
+ self.setFieldError(
+ 'name',
+ 'There is already a snap package owned by %s with this '
+ 'name.' % owner.displayname)
+
+
+class BaseSnapEditView(LaunchpadEditFormView):
schema = ISnapEditSchema
@@ -166,7 +227,7 @@
def setUpWidgets(self):
"""See `LaunchpadFormView`."""
- super(BaseSnapAddEditView, self).setUpWidgets()
+ super(BaseSnapEditView, self).setUpWidgets()
widget = self.widgets.get('vcs')
if widget is not None:
current_value = widget._getFormValue()
@@ -179,7 +240,7 @@
if 'vcs' in self.widgets:
# Set widgets as required or optional depending on the vcs
# field.
- super(BaseSnapAddEditView, self).validate_widgets(data, ['vcs'])
+ super(BaseSnapEditView, self).validate_widgets(data, ['vcs'])
vcs = data.get('vcs')
if vcs == VCSType.BZR:
self.widgets['branch'].context.required = True
@@ -189,10 +250,7 @@
self.widgets['git_ref'].context.required = True
else:
raise AssertionError("Unknown branch type %s" % vcs)
- super(BaseSnapAddEditView, self).validate_widgets(data, names=names)
-
-
-class BaseSnapEditView(BaseSnapAddEditView):
+ super(BaseSnapEditView, self).validate_widgets(data, names=names)
@action('Update snap package', name='update')
def request_action(self, action, data):
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2015-09-10 20:01:25 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2015-09-16 19:19:41 +0000
@@ -30,7 +30,10 @@
SnapEditView,
SnapView,
)
-from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
+from lp.snappy.interfaces.snap import (
+ SNAP_FEATURE_FLAG,
+ SnapFeatureDisabled,
+ )
from lp.testing import (
BrowserTestCase,
login,
@@ -53,6 +56,7 @@
find_tags_by_class,
)
from lp.testing.publication import test_traverse
+from lp.testing.views import create_initialized_view
class TestSnapNavigation(TestCaseWithFactory):
@@ -77,6 +81,111 @@
self.assertEqual(snap, obj)
+class TestSnapViewsFeatureFlag(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_feature_flag_disabled(self):
+ # Without a feature flag, we will not create new Snaps.
+ branch = self.factory.makeAnyBranch()
+ self.assertRaises(
+ SnapFeatureDisabled, create_initialized_view, branch, "+new-snap")
+
+
+class TestSnapAddView(BrowserTestCase):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestSnapAddView, self).setUp()
+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+ self.useFixture(FakeLogger())
+ self.person = self.factory.makePerson(
+ name="test-person", displayname="Test Person")
+
+ 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)
+ 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"])
+
+ def test_create_new_snap_not_logged_in(self):
+ branch = self.factory.makeAnyBranch()
+ self.assertRaises(
+ Unauthorized, self.getViewBrowser, branch, view_name="+new-snap",
+ 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("Create snap package").click()
+
+ content = find_main_content(browser.contents)
+ self.assertEqual("snap-name", extract_text(content.h1))
+ self.assertThat(
+ "Test Person", MatchesPickerText(content, "edit-owner"))
+ self.assertThat(
+ "Distribution series:\n%s\nEdit snap package" %
+ distroseries.fullseriesname,
+ MatchesTagText(content, "distro_series"))
+ self.assertThat(
+ "Source:\n%s\nEdit snap package" % source_display,
+ MatchesTagText(content, "source"))
+
+ 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("Create snap package").click()
+
+ content = find_main_content(browser.contents)
+ self.assertEqual("snap-name", extract_text(content.h1))
+ self.assertThat(
+ "Test Person", MatchesPickerText(content, "edit-owner"))
+ self.assertThat(
+ "Distribution series:\n%s\nEdit snap package" %
+ distroseries.fullseriesname,
+ MatchesTagText(content, "distro_series"))
+ self.assertThat(
+ "Source:\n%s\nEdit snap package" % source_display,
+ MatchesTagText(content, "source"))
+
+ def test_create_new_snap_users_teams_as_owner_options(self):
+ # Teams that the user is in are options for the snap package owner.
+ self.factory.makeTeam(
+ name="test-team", displayname="Test Team", members=[self.person])
+ branch = self.factory.makeAnyBranch()
+ browser = self.getViewBrowser(
+ branch, view_name="+new-snap", user=self.person)
+ options = browser.getControl("Owner").displayOptions
+ self.assertEqual(
+ ["Test Person (test-person)", "Test Team (test-team)"],
+ sorted(str(option) for option in options))
+
+
class TestSnapAdminView(BrowserTestCase):
layer = DatabaseFunctionalLayer
=== added file 'lib/lp/snappy/templates/snap-new.pt'
--- lib/lp/snappy/templates/snap-new.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/templates/snap-new.pt 2015-09-16 19:19:41 +0000
@@ -0,0 +1,40 @@
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ metal:use-macro="view/macro:page/main_side"
+ i18n:domain="launchpad">
+<body>
+
+<div metal:fill-slot="main">
+ <div>
+ <p>
+ A snap package is a self-contained application that can be installed
+ on <a href="https://developer.ubuntu.com/en/snappy/">Snappy Ubuntu
+ Core</a>. Launchpad can build snap packages using <a
+ href="https://developer.ubuntu.com/en/snappy/snapcraft/">Snapcraft</a>,
+ from any Git or Bazaar branch on Launchpad that has a
+ <tt>snapcraft.yaml</tt> file at its top level.
+ </p>
+ </div>
+
+ <div metal:use-macro="context/@@launchpad_form/form">
+ <metal:formbody fill-slot="widgets">
+ <table class="form">
+ <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/owner">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/distro_series">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ </table>
+ </metal:formbody>
+ </div>
+</div>
+
+</body>
+</html>
=== modified file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py'
--- lib/lp/snappy/tests/test_snapbuildbehaviour.py 2015-09-09 14:17:46 +0000
+++ lib/lp/snappy/tests/test_snapbuildbehaviour.py 2015-09-16 19:19:41 +0000
@@ -49,6 +49,7 @@
def setUp(self):
super(TestSnapBuildBehaviour, self).setUp()
self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+ self.pushConfig("snappy", tools_source=None)
def makeJob(self, pocket=PackagePublishingPocket.RELEASE, **kwargs):
"""Create a sample `ISnapBuildBehaviour`."""
Follow ups