← Back to team overview

launchpad-reviewers team mailing list archive

[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