launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19283
[Merge] lp:~cjwatson/launchpad/snap-edit-views into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-edit-views into lp:launchpad.
Commit message:
Add Snap:+edit, Snap:+admin, and Snap:+delete views.
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-edit-views/+merge/270193
Add Snap:+edit, Snap:+admin, and Snap:+delete views.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-edit-views into lp:launchpad.
=== modified file 'lib/lp/snappy/browser/configure.zcml'
--- lib/lp/snappy/browser/configure.zcml 2015-08-07 10:12:38 +0000
+++ lib/lp/snappy/browser/configure.zcml 2015-09-04 16:25:33 +0000
@@ -22,9 +22,30 @@
permission="launchpad.View"
name="+index"
template="../templates/snap-index.pt" />
+ <browser:menus
+ module="lp.snappy.browser.snap"
+ classes="SnapNavigationMenu" />
<browser:navigation
module="lp.snappy.browser.snap"
classes="SnapNavigation" />
+ <browser:page
+ for="lp.snappy.interfaces.snap.ISnap"
+ class="lp.snappy.browser.snap.SnapAdminView"
+ permission="launchpad.Admin"
+ name="+admin"
+ template="../../app/templates/generic-edit.pt" />
+ <browser:page
+ for="lp.snappy.interfaces.snap.ISnap"
+ class="lp.snappy.browser.snap.SnapEditView"
+ permission="launchpad.Edit"
+ name="+edit"
+ template="../templates/snap-edit.pt" />
+ <browser:page
+ for="lp.snappy.interfaces.snap.ISnap"
+ class="lp.snappy.browser.snap.SnapDeleteView"
+ permission="launchpad.Edit"
+ name="+delete"
+ template="../templates/snap-delete.pt" />
<adapter
provides="lp.services.webapp.interfaces.IBreadcrumb"
for="lp.snappy.interfaces.snap.ISnap"
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2015-08-27 16:06:41 +0000
+++ lib/lp/snappy/browser/snap.py 2015-09-04 16:25:33 +0000
@@ -5,14 +5,41 @@
__metaclass__ = type
__all__ = [
+ 'SnapDeleteView',
+ 'SnapEditView',
'SnapNavigation',
+ 'SnapNavigationMenu',
'SnapView',
]
+from lazr.restful.interface import (
+ copy_field,
+ use_template,
+ )
+from zope.component import getUtility
+from zope.interface import Interface
+from zope.schema import Choice
+
+from lp.app.browser.launchpadform import (
+ action,
+ custom_widget,
+ LaunchpadEditFormView,
+ render_radio_widget_part,
+ )
+from lp.app.browser.lazrjs import (
+ InlinePersonEditPickerWidget,
+ TextLineEditorWidget,
+ )
+from lp.app.browser.tales import format_link
+from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
+from lp.registry.enums import VCSType
from lp.services.webapp import (
canonical_url,
+ enabled_with_permission,
LaunchpadView,
+ Link,
Navigation,
+ NavigationMenu,
stepthrough,
)
from lp.services.webapp.authorization import check_permission
@@ -20,7 +47,11 @@
Breadcrumb,
NameBreadcrumb,
)
-from lp.snappy.interfaces.snap import ISnap
+from lp.snappy.interfaces.snap import (
+ ISnap,
+ ISnapSet,
+ NoSuchSnap,
+ )
from lp.snappy.interfaces.snapbuild import ISnapBuildSet
from lp.soyuz.browser.build import get_build_by_id_str
@@ -46,6 +77,28 @@
text="Snap packages", inside=self.context.owner)
+class SnapNavigationMenu(NavigationMenu):
+ """Navigation menu for snap packages."""
+
+ usedfor = ISnap
+
+ facet = 'overview'
+
+ links = ('admin', 'delete', 'edit')
+
+ @enabled_with_permission('launchpad.Admin')
+ def admin(self):
+ return Link('+admin', 'Administer snap package', icon='edit')
+
+ @enabled_with_permission('launchpad.Edit')
+ def edit(self):
+ return Link('+edit', 'Edit snap package', icon='edit')
+
+ @enabled_with_permission('launchpad.Edit')
+ def delete(self):
+ return Link('+delete', 'Delete snap package', icon='trash-icon')
+
+
class SnapView(LaunchpadView):
"""Default view of a Snap."""
@@ -54,6 +107,22 @@
return builds_for_snap(self.context)
@property
+ def name_widget(self):
+ name = ISnap['name']
+ title = "Edit the snap package name"
+ return TextLineEditorWidget(
+ self.context, name, title, 'h1', max_width='95%', truncate_lines=1)
+
+ @property
+ def person_picker(self):
+ field = copy_field(
+ ISnap['owner'],
+ vocabularyName='UserTeamsParticipationPlusSelfSimpleDisplay')
+ return InlinePersonEditPickerWidget(
+ self.context, field, format_link(self.context.owner),
+ header='Change owner', step_title='Select a new owner')
+
+ @property
def source(self):
if self.context.branch is not None:
return self.context.branch
@@ -86,3 +155,161 @@
if len(builds) >= 10:
break
return builds
+
+
+class ISnapEditSchema(Interface):
+ """Schema for adding or editing a snap package."""
+
+ use_template(ISnap, include=[
+ 'owner',
+ 'name',
+ 'require_virtualized',
+ ])
+ distro_series = Choice(
+ vocabulary='BuildableDistroSeries', title=u'Distribution series')
+ vcs = Choice(vocabulary=VCSType, required=True, title=u'VCS')
+
+ # Each of these is only required if vcs has an appropriate value. Later
+ # validation takes care of adjusting the required attribute.
+ branch = copy_field(ISnap['branch'], required=True)
+ git_repository = copy_field(ISnap['git_repository'], required=True)
+ git_path = copy_field(ISnap['git_path'], required=True)
+
+
+class BaseSnapAddEditView(LaunchpadEditFormView):
+
+ schema = ISnapEditSchema
+
+ @property
+ def cancel_url(self):
+ return canonical_url(self.context)
+
+ def setUpWidgets(self):
+ """See `LaunchpadFormView`."""
+ super(BaseSnapAddEditView, self).setUpWidgets()
+ widget = self.widgets.get('vcs')
+ if widget is not None:
+ current_value = widget._getFormValue()
+ self.vcs_bzr, self.vcs_git = [
+ render_radio_widget_part(widget, value, current_value)
+ for value in (VCSType.BZR, VCSType.GIT)]
+
+ def validate_widgets(self, data, names=None):
+ """See `LaunchpadFormView`."""
+ if 'vcs' in self.widgets:
+ # Set widgets as required or optional depending on the vcs
+ # field.
+ super(BaseSnapAddEditView, self).validate_widgets(data, ['vcs'])
+ vcs = data.get('vcs')
+ if vcs == VCSType.BZR:
+ self.widgets['branch'].context.required = True
+ self.widgets['git_repository'].context.required = False
+ self.widgets['git_path'].context.required = False
+ elif vcs == VCSType.GIT:
+ self.widgets['branch'].context.required = False
+ self.widgets['git_repository'].context.required = True
+ self.widgets['git_path'].context.required = True
+ else:
+ raise AssertionError("Unknown branch type %s" % vcs)
+ super(BaseSnapAddEditView, self).validate_widgets(data, names=names)
+
+
+class BaseSnapEditView(BaseSnapAddEditView):
+
+ @action('Update snap package', name='update')
+ def request_action(self, action, data):
+ vcs = data.pop('vcs', None)
+ if vcs == VCSType.BZR:
+ data['git_repository'] = None
+ data['git_path'] = None
+ elif vcs == VCSType.GIT:
+ data['branch'] = None
+ self.updateContextFromData(data)
+ self.next_url = canonical_url(self.context)
+
+ @property
+ def adapters(self):
+ """See `LaunchpadFormView`."""
+ return {ISnapEditSchema: self.context}
+
+
+class SnapAdminView(BaseSnapEditView):
+ """View for administering snap packages."""
+
+ @property
+ def title(self):
+ return 'Administer %s snap package' % self.context.name
+
+ label = title
+
+ field_names = ['require_virtualized']
+
+ @property
+ def initial_values(self):
+ return {'require_virtualized': self.context.require_virtualized}
+
+
+class SnapEditView(BaseSnapEditView):
+ """View for editing snap packages."""
+
+ @property
+ def title(self):
+ return 'Edit %s snap package' % self.context.name
+
+ label = title
+
+ field_names = [
+ 'owner', 'name', 'distro_series', 'vcs', 'branch', 'git_repository',
+ 'git_path']
+ custom_widget('distro_series', LaunchpadRadioWidget)
+ custom_widget('vcs', LaunchpadRadioWidget)
+
+ @property
+ def initial_values(self):
+ if self.context.git_repository is not None:
+ vcs = VCSType.GIT
+ else:
+ vcs = VCSType.BZR
+ return {
+ 'distro_series': self.context.distro_series,
+ 'vcs': vcs,
+ }
+
+ def validate(self, data):
+ super(SnapEditView, self).validate(data)
+ owner = data.get('owner', None)
+ name = data.get('name', None)
+ if owner and name:
+ try:
+ snap = getUtility(ISnapSet).getByName(owner, name)
+ if snap != self.context:
+ self.setFieldError(
+ 'name',
+ 'There is already a snap package owned by %s with '
+ 'this name.' % owner.displayname)
+ except NoSuchSnap:
+ pass
+
+
+class SnapDeleteView(BaseSnapEditView):
+ """View for deleting snap packages."""
+
+ @property
+ def title(self):
+ return 'Delete %s snap package' % self.context.name
+
+ label = title
+
+ field_names = []
+
+ @property
+ def has_builds(self):
+ return not self.context.builds.is_empty()
+
+ @action('Delete snap package', name='delete')
+ def delete_action(self, action, data):
+ owner = self.context.owner
+ self.context.destroySelf()
+ # XXX cjwatson 2015-07-17: This should go to Person:+snaps or
+ # similar (or something on SnapSet?) once that exists.
+ self.next_url = canonical_url(owner)
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2015-08-27 16:06:41 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2015-09-04 16:25:33 +0000
@@ -10,18 +10,31 @@
timedelta,
)
+from fixtures import FakeLogger
+from mechanize import LinkNotFoundError
import pytz
from zope.component import getUtility
+from zope.publisher.interfaces import NotFound
+from zope.security.interfaces import Unauthorized
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.interfaces.processor import IProcessorSet
+from lp.registry.interfaces.series import SeriesStatus
+from lp.services.database.constants import UTC_NOW
from lp.services.features.testing import FeatureFixture
from lp.services.webapp import canonical_url
-from lp.snappy.browser.snap import SnapView
+from lp.services.webapp.servers import LaunchpadTestRequest
+from lp.snappy.browser.snap import (
+ SnapAdminView,
+ SnapEditView,
+ SnapView,
+ )
from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
from lp.testing import (
BrowserTestCase,
+ login,
+ login_person,
person_logged_in,
TestCaseWithFactory,
time_counter,
@@ -30,6 +43,15 @@
DatabaseFunctionalLayer,
LaunchpadFunctionalLayer,
)
+from lp.testing.matchers import (
+ MatchesPickerText,
+ MatchesTagText,
+ )
+from lp.testing.pages import (
+ extract_text,
+ find_main_content,
+ find_tags_by_class,
+ )
from lp.testing.publication import test_traverse
@@ -55,6 +77,184 @@
self.assertEqual(snap, obj)
+class TestSnapAdminView(BrowserTestCase):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestSnapAdminView, 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_unauthorized(self):
+ # A non-admin user cannot administer a snap package.
+ login_person(self.person)
+ snap = self.factory.makeSnap(registrant=self.person)
+ snap_url = canonical_url(snap)
+ browser = self.getViewBrowser(snap, user=self.person)
+ self.assertRaises(
+ LinkNotFoundError, browser.getLink, "Administer snap package")
+ self.assertRaises(
+ Unauthorized, self.getUserBrowser, snap_url + "/+admin",
+ user=self.person)
+
+ def test_admin_snap(self):
+ # Admins can change require_virtualized.
+ login("admin@xxxxxxxxxxxxx")
+ commercial_admin = self.factory.makePerson(
+ member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
+ login_person(self.person)
+ snap = self.factory.makeSnap(registrant=self.person)
+ self.assertTrue(snap.require_virtualized)
+ browser = self.getViewBrowser(snap, user=commercial_admin)
+ browser.getLink("Administer snap package").click()
+ browser.getControl("Require virtualized builders").selected = False
+ browser.getControl("Update snap package").click()
+ login_person(self.person)
+ self.assertFalse(snap.require_virtualized)
+
+ def test_admin_snap_sets_date_last_modified(self):
+ # Administering a snap package sets the date_last_modified property.
+ login("admin@xxxxxxxxxxxxx")
+ commercial_admin = self.factory.makePerson(
+ member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
+ login_person(self.person)
+ date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
+ snap = self.factory.makeSnap(
+ registrant=self.person, date_created=date_created)
+ login_person(commercial_admin)
+ view = SnapAdminView(snap, LaunchpadTestRequest())
+ view.initialize()
+ view.request_action.success({"require_virtualized": False})
+ self.assertSqlAttributeEqualsDate(snap, "date_last_modified", UTC_NOW)
+
+
+class TestSnapEditView(BrowserTestCase):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestSnapEditView, 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_edit_snap(self):
+ archive = self.factory.makeArchive()
+ old_series = self.factory.makeDistroSeries(
+ distribution=archive.distribution, status=SeriesStatus.CURRENT)
+ 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_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("Git", index=0).click()
+ browser.getControl("Git repository").value = (
+ new_git_ref.repository.identity)
+ browser.getControl("Git branch path").value = new_git_ref.path
+ browser.getControl("Update snap package").click()
+
+ content = find_main_content(browser.contents)
+ self.assertEqual("new-name\nEdit", extract_text(content.h1))
+ self.assertThat("New Team", MatchesPickerText(content, "edit-owner"))
+ self.assertThat(
+ "Distribution series:\n%s\nEdit snap package" %
+ new_series.fullseriesname,
+ MatchesTagText(content, "distro_series"))
+ self.assertThat(
+ "Source:\n%s\nEdit snap package" % new_git_ref.display_name,
+ MatchesTagText(content, "source"))
+
+ def test_edit_snap_sets_date_last_modified(self):
+ # Editing a snap package sets the date_last_modified property.
+ date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
+ snap = self.factory.makeSnap(
+ registrant=self.person, date_created=date_created)
+ with person_logged_in(self.person):
+ view = SnapEditView(snap, LaunchpadTestRequest())
+ view.initialize()
+ view.request_action.success({
+ "owner": snap.owner,
+ "name": u"changed",
+ "distro_series": snap.distro_series,
+ })
+ self.assertSqlAttributeEqualsDate(snap, "date_last_modified", UTC_NOW)
+
+ def test_edit_snap_already_exists(self):
+ snap = self.factory.makeSnap(
+ registrant=self.person, owner=self.person, name=u"one")
+ self.factory.makeSnap(
+ 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("Update snap package").click()
+ self.assertEqual(
+ "There is already a snap package owned by Test Person with this "
+ "name.",
+ extract_text(find_tags_by_class(browser.contents, "message")[1]))
+
+
+class TestSnapDeleteView(BrowserTestCase):
+
+ layer = LaunchpadFunctionalLayer
+
+ def setUp(self):
+ super(TestSnapDeleteView, self).setUp()
+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+ self.person = self.factory.makePerson(
+ name="test-person", displayname="Test Person")
+
+ def test_unauthorized(self):
+ # A user without edit access cannot delete a snap package.
+ self.useFixture(FakeLogger())
+ snap = self.factory.makeSnap(registrant=self.person, owner=self.person)
+ snap_url = canonical_url(snap)
+ other_person = self.factory.makePerson()
+ browser = self.getViewBrowser(snap, user=other_person)
+ self.assertRaises(
+ LinkNotFoundError, browser.getLink, "Delete snap package")
+ self.assertRaises(
+ Unauthorized, self.getUserBrowser, snap_url + "/+delete",
+ user=other_person)
+
+ def test_delete_snap_without_builds(self):
+ # A snap package without builds can be deleted.
+ self.useFixture(FakeLogger())
+ snap = self.factory.makeSnap(registrant=self.person, owner=self.person)
+ snap_url = canonical_url(snap)
+ owner_url = canonical_url(self.person)
+ browser = self.getViewBrowser(snap, user=self.person)
+ browser.getLink("Delete snap package").click()
+ browser.getControl("Delete snap package").click()
+ self.assertEqual(owner_url, browser.url)
+ self.assertRaises(NotFound, browser.open, snap_url)
+
+ def test_delete_snap_with_builds(self):
+ # A snap package with builds cannot be deleted.
+ snap = self.factory.makeSnap(registrant=self.person, owner=self.person)
+ self.factory.makeSnapBuild(snap=snap)
+ browser = self.getViewBrowser(snap, user=self.person)
+ browser.getLink("Delete snap package").click()
+ self.assertIn("This snap package cannot be deleted", browser.contents)
+ self.assertRaises(
+ LookupError, browser.getControl, "Delete snap package")
+
+
class TestSnapView(BrowserTestCase):
layer = LaunchpadFunctionalLayer
=== added directory 'lib/lp/snappy/javascript'
=== added file 'lib/lp/snappy/javascript/snap.edit.js'
--- lib/lp/snappy/javascript/snap.edit.js 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/javascript/snap.edit.js 2015-09-04 16:25:33 +0000
@@ -0,0 +1,36 @@
+/* Copyright 2015 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Control enabling/disabling form elements on the Snap:+edit page.
+ *
+ * @module Y.lp.snappy.snap.edit
+ * @requires node, DOM
+ */
+YUI.add('lp.snappy.snap.edit', function(Y) {
+ Y.log('loading lp.snappy.snap.edit');
+ var module = Y.namespace('lp.snappy.snap.edit');
+
+ module.set_enabled = function(field_id, is_enabled) {
+ var field = Y.DOM.byId(field_id);
+ field.disabled = !is_enabled;
+ };
+
+ module.onclick_vcs = function(e) {
+ var selected_vcs = null;
+ Y.all('input[name="field.vcs"]').each(function(node) {
+ if (node.get('checked')) {
+ selected_vcs = node.get('value');
+ }
+ });
+ module.set_enabled('field.branch', selected_vcs === 'BZR');
+ module.set_enabled('field.git_repository', selected_vcs === 'GIT');
+ module.set_enabled('field.git_path', selected_vcs === 'GIT');
+ };
+
+ module.setup = function() {
+ Y.all('input[name="field.vcs"]').on('click', module.onclick_vcs);
+
+ // Set the initial state.
+ module.onclick_vcs();
+ };
+}, '0.1', {'requires': ['node', 'DOM']});
=== added directory 'lib/lp/snappy/javascript/tests'
=== added file 'lib/lp/snappy/javascript/tests/test_snap.edit.html'
--- lib/lp/snappy/javascript/tests/test_snap.edit.html 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/javascript/tests/test_snap.edit.html 2015-09-04 16:25:33 +0000
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 Canonical Ltd. This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<html>
+ <head>
+ <title>snappy.snap.edit Tests</title>
+
+ <!-- YUI and test setup -->
+ <script type="text/javascript"
+ src="../../../../../build/js/yui/yui/yui.js">
+ </script>
+ <link rel="stylesheet"
+ href="../../../../../build/js/yui/console/assets/console-core.css" />
+ <link rel="stylesheet"
+ href="../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
+ <link rel="stylesheet"
+ href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
+
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
+
+ <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
+
+ <!-- Dependencies -->
+ <script type="text/javascript"
+ src="../../../../../build/js/lp/app/lp.js"></script>
+
+ <!-- The module under test. -->
+ <script type="text/javascript" src="../snap.edit.js"></script>
+
+ <!-- The test suite -->
+ <script type="text/javascript" src="test_snap.edit.js"></script>
+
+ <script type="text/javascript">
+ YUI().use('lp.snappy.snap.edit', function(Y) {
+ Y.on('domready', Y.lp.snappy.snap.edit.setup);
+ });
+ </script>
+
+ </head>
+ <body class="yui3-skin-sam">
+ <ul id="suites">
+ <li>lp.snappy.snap.edit.test</li>
+ </ul>
+ <div id="snap.edit">
+ <form action="." name="launchpadform" method="post"
+ enctype="multipart/form-data"
+ accept-charset="UTF-8">
+
+ <table class="form">
+ <tr>
+ <td colspan="2">
+ <div>
+ <label for="field.vcs">Source:</label>
+ <table>
+ <tr>
+ <td>
+ <label>
+ <input class="radioType"
+ id="field.vcs.Bazaar"
+ name="field.vcs"
+ type="radio"
+ value="BZR" />
+ Bazaar
+ </label>
+ <table class="subordinate">
+ <tr>
+ <td colspan="2">
+ <div>
+ <label for="field.branch">Bazaar branch:</label>
+ <div>
+ <input type="text"
+ value=""
+ id="field.branch"
+ name="field.branch"
+ size="35"
+ class="" />
+ </div>
+ </div>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <label>
+ <input class="radioType"
+ id="field.vcs.Git"
+ name="field.vcs"
+ type="radio"
+ value="GIT" />
+ Git
+ </label>
+ <table class="subordinate">
+ <tr>
+ <td colspan="2">
+ <div>
+ <label for="field.git_repository">Git repository:</label>
+ <div>
+ <input type="text"
+ value=""
+ id="field.git_repository"
+ name="field.git_repository"
+ size="20"
+ class="" />
+ </div>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ <div>
+ <label for="field.git_path">Git path:</label>
+ <div>
+ <input class="textType"
+ id="field.git_path"
+ name="field.git_path"
+ size="20"
+ type="text"
+ value="" />
+ </div>
+ </div>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </td>
+ </tr>
+ </table>
+
+ <input type="submit" id="field.actions.update"
+ name="field.actions.update" value="Update snap package"
+ class="button" />
+ or
+ <a href="https://launchpad.dev/~me/+snap/s">Cancel</a>
+ </form>
+ </div>
+ </body>
+</html>
=== added file 'lib/lp/snappy/javascript/tests/test_snap.edit.js'
--- lib/lp/snappy/javascript/tests/test_snap.edit.js 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/javascript/tests/test_snap.edit.js 2015-09-04 16:25:33 +0000
@@ -0,0 +1,80 @@
+/* Copyright 2015 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Test driver for snap.edit.js.
+ */
+YUI.add('lp.snappy.snap.edit.test', function(Y) {
+ var tests = Y.namespace('lp.snappy.snap.edit.test');
+ var module = Y.lp.snappy.snap.edit;
+ tests.suite = new Y.Test.Suite('snappy.snap.edit Tests');
+
+ tests.suite.add(new Y.Test.Case({
+ name: 'snappy.snap.edit_tests',
+
+ setUp: function() {
+ this.tbody = Y.one('#snap.edit');
+
+ // Get the individual VCS type radio buttons.
+ this.vcs_bzr = Y.DOM.byId('field.vcs.Bazaar');
+ this.vcs_git = Y.DOM.byId('field.vcs.Git');
+
+ // Get the input widgets.
+ this.input_branch = Y.DOM.byId('field.branch');
+ this.input_git_repository = Y.DOM.byId('field.git_repository');
+ this.input_git_path = Y.DOM.byId('field.git_path');
+ },
+
+ tearDown: function() {
+ delete this.tbody;
+ },
+
+ test_handlers_connected: function() {
+ // Manually invoke the setup function to ensure the handlers are
+ // set.
+ module.setup();
+
+ var check_handler = function(field, expected) {
+ var custom_events = Y.Event.getListeners(field, 'click');
+ var click_event = custom_events[0];
+ var subscribers = click_event.subscribers;
+ Y.each(subscribers, function(sub) {
+ Y.Assert.isTrue(sub.contains(expected),
+ 'handler not set up');
+ });
+ };
+
+ check_handler(this.vcs_bzr, module.onclick_vcs);
+ check_handler(this.vcs_git, module.onclick_vcs);
+ },
+
+ test_select_vcs_bzr: function() {
+ this.vcs_bzr.checked = true;
+ module.onclick_vcs();
+ // The branch input field is enabled.
+ Y.Assert.isFalse(this.input_branch.disabled,
+ 'branch field disabled');
+ // The git_repository and git_path input fields are disabled.
+ Y.Assert.isTrue(this.input_git_repository.disabled,
+ 'git_repository field not disabled');
+ Y.Assert.isTrue(this.input_git_path.disabled,
+ 'git_path field not disabled');
+ },
+
+ test_select_vcs_git: function() {
+ this.vcs_git.checked = true;
+ module.onclick_vcs();
+ // The branch input field is disabled.
+ Y.Assert.isTrue(this.input_branch.disabled,
+ 'branch field not disabled');
+ // The git_repository and git_path input fields are enabled.
+ Y.Assert.isFalse(this.input_git_repository.disabled,
+ 'git_repository field disabled');
+ Y.Assert.isFalse(this.input_git_path.disabled,
+ 'git_path field disabled');
+ }
+ }));
+}, '0.1', {
+ requires: ['lp.testing.runner', 'test', 'test-console',
+ 'Event', 'node-event-simulate',
+ 'lp.snappy.snap.edit']
+});
=== added file 'lib/lp/snappy/templates/snap-delete.pt'
--- lib/lp/snappy/templates/snap-delete.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/templates/snap-delete.pt 2015-09-04 16:25:33 +0000
@@ -0,0 +1,21 @@
+<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_only"
+ i18n:domain="launchpad">
+<body>
+
+ <div metal:fill-slot="main">
+ <tal:has-builds condition="view/has_builds">
+ This snap package cannot be deleted as builds have been created for it.
+ </tal:has-builds>
+
+ <tal:has-no-builds condition="not: view/has_builds">
+ <div metal:use-macro="context/@@launchpad_form/form"/>
+ </tal:has-no-builds>
+ </div>
+
+</body>
+</html>
=== added file 'lib/lp/snappy/templates/snap-edit.pt'
--- lib/lp/snappy/templates/snap-edit.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/templates/snap-edit.pt 2015-09-04 16:25:33 +0000
@@ -0,0 +1,79 @@
+<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_only"
+ i18n:domain="launchpad">
+<body>
+
+<metal:block fill-slot="head_epilogue">
+ <style type="text/css">
+ .subordinate {
+ margin: 0.5em 0 0.5em 4em;
+ }
+ </style>
+</metal:block>
+
+<div metal:fill-slot="main">
+ <div metal:use-macro="context/@@launchpad_form/form">
+ <metal:formbody fill-slot="widgets">
+ <table class="form">
+ <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/name">
+ <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>
+
+ <tr>
+ <td>
+ <div>
+ <label for="field.vcs">Source:</label>
+ <table>
+ <tr>
+ <td>
+ <label tal:replace="structure view/vcs_bzr" />
+ <table class="subordinate">
+ <tal:widget define="widget nocall:view/widgets/branch">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <label tal:replace="structure view/vcs_git" />
+ <table class="subordinate">
+ <tal:widget define="widget nocall:view/widgets/git_repository">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ <tal:widget define="widget nocall:view/widgets/git_path">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ </table>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </td>
+ </tr>
+ </table>
+ </metal:formbody>
+ </div>
+
+ <script type="text/javascript">
+ LPJS.use('lp.snappy.snap.edit', function(Y) {
+ Y.on('domready', function(e) {
+ Y.lp.snappy.snap.edit.setup();
+ }, window);
+ });
+ </script>
+</div>
+
+</body>
+</html>
=== modified file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt 2015-08-27 16:06:41 +0000
+++ lib/lp/snappy/templates/snap-index.pt 2015-09-04 16:25:33 +0000
@@ -22,7 +22,7 @@
</metal:side>
<metal:heading fill-slot="heading">
- <h1 tal:content="context/name"/>
+ <h1 tal:replace="structure view/name_widget"/>
</metal:heading>
<div metal:fill-slot="main">
@@ -30,18 +30,22 @@
<div class="two-column-list">
<dl id="owner">
<dt>Owner:</dt>
- <dd tal:content="structure context/owner/fmt:link"/>
+ <dd tal:content="structure view/person_picker"/>
</dl>
<dl id="distro_series">
<dt>Distribution series:</dt>
<dd tal:define="distro_series context/distro_series">
<a tal:attributes="href distro_series/fmt:url"
tal:content="distro_series/fullseriesname"/>
+ <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
</dd>
</dl>
<dl id="source" tal:define="source view/source" tal:condition="source">
<dt>Source:</dt>
- <dd tal:content="structure source/fmt:link"/>
+ <dd>
+ <a tal:replace="structure source/fmt:link"/>
+ <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
+ </dd>
</dl>
</div>
=== added file 'lib/lp/snappy/tests/test_yuitests.py'
--- lib/lp/snappy/tests/test_yuitests.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_yuitests.py 2015-09-04 16:25:33 +0000
@@ -0,0 +1,24 @@
+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Run YUI.test tests."""
+
+__metaclass__ = type
+__all__ = []
+
+from lp.testing import (
+ build_yui_unittest_suite,
+ YUIUnitTestCase,
+ )
+from lp.testing.layers import YUITestLayer
+
+
+class SnappyYUIUnitTestCase(YUIUnitTestCase):
+
+ layer = YUITestLayer
+ suite_name = 'SnappyYUIUnitTests'
+
+
+def test_suite():
+ app_testing_path = 'lp/snappy'
+ return build_yui_unittest_suite(app_testing_path, SnappyYUIUnitTestCase)
Follow ups