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

Proposed by Colin Watson
Status: Merged
Merged at revision: 17715
Proposed branch: lp:~cjwatson/launchpad/snap-edit-views
Merge into: lp:launchpad
Diff against target: 975 lines (+825/-4)
10 files modified
lib/lp/snappy/browser/configure.zcml (+21/-0)
lib/lp/snappy/browser/snap.py (+211/-1)
lib/lp/snappy/browser/tests/test_snap.py (+201/-1)
lib/lp/snappy/javascript/snap.edit.js (+36/-0)
lib/lp/snappy/javascript/tests/test_snap.edit.html (+146/-0)
lib/lp/snappy/javascript/tests/test_snap.edit.js (+80/-0)
lib/lp/snappy/templates/snap-delete.pt (+21/-0)
lib/lp/snappy/templates/snap-edit.pt (+79/-0)
lib/lp/snappy/templates/snap-index.pt (+6/-2)
lib/lp/snappy/tests/test_yuitests.py (+24/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-edit-views
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+270193@code.launchpad.net

Commit message

Add Snap:+edit, Snap:+admin, and Snap:+delete views.

Description of the change

Add Snap:+edit, Snap:+admin, and Snap:+delete views.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/snappy/browser/configure.zcml'
2--- lib/lp/snappy/browser/configure.zcml 2015-08-07 10:12:38 +0000
3+++ lib/lp/snappy/browser/configure.zcml 2015-09-07 16:06:24 +0000
4@@ -22,9 +22,30 @@
5 permission="launchpad.View"
6 name="+index"
7 template="../templates/snap-index.pt" />
8+ <browser:menus
9+ module="lp.snappy.browser.snap"
10+ classes="SnapNavigationMenu" />
11 <browser:navigation
12 module="lp.snappy.browser.snap"
13 classes="SnapNavigation" />
14+ <browser:page
15+ for="lp.snappy.interfaces.snap.ISnap"
16+ class="lp.snappy.browser.snap.SnapAdminView"
17+ permission="launchpad.Admin"
18+ name="+admin"
19+ template="../../app/templates/generic-edit.pt" />
20+ <browser:page
21+ for="lp.snappy.interfaces.snap.ISnap"
22+ class="lp.snappy.browser.snap.SnapEditView"
23+ permission="launchpad.Edit"
24+ name="+edit"
25+ template="../templates/snap-edit.pt" />
26+ <browser:page
27+ for="lp.snappy.interfaces.snap.ISnap"
28+ class="lp.snappy.browser.snap.SnapDeleteView"
29+ permission="launchpad.Edit"
30+ name="+delete"
31+ template="../templates/snap-delete.pt" />
32 <adapter
33 provides="lp.services.webapp.interfaces.IBreadcrumb"
34 for="lp.snappy.interfaces.snap.ISnap"
35
36=== modified file 'lib/lp/snappy/browser/snap.py'
37--- lib/lp/snappy/browser/snap.py 2015-08-27 16:06:41 +0000
38+++ lib/lp/snappy/browser/snap.py 2015-09-07 16:06:24 +0000
39@@ -5,14 +5,38 @@
40
41 __metaclass__ = type
42 __all__ = [
43+ 'SnapDeleteView',
44+ 'SnapEditView',
45 'SnapNavigation',
46+ 'SnapNavigationMenu',
47 'SnapView',
48 ]
49
50+from lazr.restful.interface import (
51+ copy_field,
52+ use_template,
53+ )
54+from zope.component import getUtility
55+from zope.interface import Interface
56+from zope.schema import Choice
57+
58+from lp.app.browser.launchpadform import (
59+ action,
60+ custom_widget,
61+ LaunchpadEditFormView,
62+ render_radio_widget_part,
63+ )
64+from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
65+from lp.app.browser.tales import format_link
66+from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
67+from lp.registry.enums import VCSType
68 from lp.services.webapp import (
69 canonical_url,
70+ enabled_with_permission,
71 LaunchpadView,
72+ Link,
73 Navigation,
74+ NavigationMenu,
75 stepthrough,
76 )
77 from lp.services.webapp.authorization import check_permission
78@@ -20,7 +44,11 @@
79 Breadcrumb,
80 NameBreadcrumb,
81 )
82-from lp.snappy.interfaces.snap import ISnap
83+from lp.snappy.interfaces.snap import (
84+ ISnap,
85+ ISnapSet,
86+ NoSuchSnap,
87+ )
88 from lp.snappy.interfaces.snapbuild import ISnapBuildSet
89 from lp.soyuz.browser.build import get_build_by_id_str
90
91@@ -46,6 +74,28 @@
92 text="Snap packages", inside=self.context.owner)
93
94
95+class SnapNavigationMenu(NavigationMenu):
96+ """Navigation menu for snap packages."""
97+
98+ usedfor = ISnap
99+
100+ facet = 'overview'
101+
102+ links = ('edit', 'delete', 'admin')
103+
104+ @enabled_with_permission('launchpad.Admin')
105+ def admin(self):
106+ return Link('+admin', 'Administer snap package', icon='edit')
107+
108+ @enabled_with_permission('launchpad.Edit')
109+ def edit(self):
110+ return Link('+edit', 'Edit snap package', icon='edit')
111+
112+ @enabled_with_permission('launchpad.Edit')
113+ def delete(self):
114+ return Link('+delete', 'Delete snap package', icon='trash-icon')
115+
116+
117 class SnapView(LaunchpadView):
118 """Default view of a Snap."""
119
120@@ -54,6 +104,15 @@
121 return builds_for_snap(self.context)
122
123 @property
124+ def person_picker(self):
125+ field = copy_field(
126+ ISnap['owner'],
127+ vocabularyName='UserTeamsParticipationPlusSelfSimpleDisplay')
128+ return InlinePersonEditPickerWidget(
129+ self.context, field, format_link(self.context.owner),
130+ header='Change owner', step_title='Select a new owner')
131+
132+ @property
133 def source(self):
134 if self.context.branch is not None:
135 return self.context.branch
136@@ -86,3 +145,154 @@
137 if len(builds) >= 10:
138 break
139 return builds
140+
141+
142+class ISnapEditSchema(Interface):
143+ """Schema for adding or editing a snap package."""
144+
145+ use_template(ISnap, include=[
146+ 'owner',
147+ 'name',
148+ 'require_virtualized',
149+ ])
150+ distro_series = Choice(
151+ vocabulary='BuildableDistroSeries', title=u'Distribution series')
152+ vcs = Choice(vocabulary=VCSType, required=True, title=u'VCS')
153+
154+ # Each of these is only required if vcs has an appropriate value. Later
155+ # validation takes care of adjusting the required attribute.
156+ branch = copy_field(ISnap['branch'], required=True)
157+ git_repository = copy_field(ISnap['git_repository'], required=True)
158+ git_path = copy_field(ISnap['git_path'], required=True)
159+
160+
161+class BaseSnapAddEditView(LaunchpadEditFormView):
162+
163+ schema = ISnapEditSchema
164+
165+ @property
166+ def cancel_url(self):
167+ return canonical_url(self.context)
168+
169+ def setUpWidgets(self):
170+ """See `LaunchpadFormView`."""
171+ super(BaseSnapAddEditView, self).setUpWidgets()
172+ widget = self.widgets.get('vcs')
173+ if widget is not None:
174+ current_value = widget._getFormValue()
175+ self.vcs_bzr_radio, self.vcs_git_radio = [
176+ render_radio_widget_part(widget, value, current_value)
177+ for value in (VCSType.BZR, VCSType.GIT)]
178+
179+ def validate_widgets(self, data, names=None):
180+ """See `LaunchpadFormView`."""
181+ if 'vcs' in self.widgets:
182+ # Set widgets as required or optional depending on the vcs
183+ # field.
184+ super(BaseSnapAddEditView, self).validate_widgets(data, ['vcs'])
185+ vcs = data.get('vcs')
186+ if vcs == VCSType.BZR:
187+ self.widgets['branch'].context.required = True
188+ self.widgets['git_repository'].context.required = False
189+ self.widgets['git_path'].context.required = False
190+ elif vcs == VCSType.GIT:
191+ self.widgets['branch'].context.required = False
192+ self.widgets['git_repository'].context.required = True
193+ self.widgets['git_path'].context.required = True
194+ else:
195+ raise AssertionError("Unknown branch type %s" % vcs)
196+ super(BaseSnapAddEditView, self).validate_widgets(data, names=names)
197+
198+
199+class BaseSnapEditView(BaseSnapAddEditView):
200+
201+ @action('Update snap package', name='update')
202+ def request_action(self, action, data):
203+ vcs = data.pop('vcs', None)
204+ if vcs == VCSType.BZR:
205+ data['git_repository'] = None
206+ data['git_path'] = None
207+ elif vcs == VCSType.GIT:
208+ data['branch'] = None
209+ self.updateContextFromData(data)
210+ self.next_url = canonical_url(self.context)
211+
212+ @property
213+ def adapters(self):
214+ """See `LaunchpadFormView`."""
215+ return {ISnapEditSchema: self.context}
216+
217+
218+class SnapAdminView(BaseSnapEditView):
219+ """View for administering snap packages."""
220+
221+ @property
222+ def label(self):
223+ return 'Administer %s snap package' % self.context.name
224+
225+ page_title = 'Administer'
226+
227+ field_names = ['require_virtualized']
228+
229+
230+class SnapEditView(BaseSnapEditView):
231+ """View for editing snap packages."""
232+
233+ @property
234+ def label(self):
235+ return 'Edit %s snap package' % self.context.name
236+
237+ page_title = 'Edit'
238+
239+ field_names = [
240+ 'owner', 'name', 'distro_series', 'vcs', 'branch', 'git_repository',
241+ 'git_path']
242+ custom_widget('distro_series', LaunchpadRadioWidget)
243+ custom_widget('vcs', LaunchpadRadioWidget)
244+
245+ @property
246+ def initial_values(self):
247+ if self.context.git_repository is not None:
248+ vcs = VCSType.GIT
249+ else:
250+ vcs = VCSType.BZR
251+ return {'vcs': vcs}
252+
253+ def validate(self, data):
254+ super(SnapEditView, self).validate(data)
255+ owner = data.get('owner', None)
256+ name = data.get('name', None)
257+ if owner and name:
258+ try:
259+ snap = getUtility(ISnapSet).getByName(owner, name)
260+ if snap != self.context:
261+ self.setFieldError(
262+ 'name',
263+ 'There is already a snap package owned by %s with '
264+ 'this name.' % owner.displayname)
265+ except NoSuchSnap:
266+ pass
267+
268+
269+class SnapDeleteView(BaseSnapEditView):
270+ """View for deleting snap packages."""
271+
272+ @property
273+ def label(self):
274+ return 'Delete %s snap package' % self.context.name
275+
276+ page_title = 'Delete'
277+
278+ field_names = []
279+
280+ @property
281+ def has_builds(self):
282+ return not self.context.builds.is_empty()
283+
284+ @action('Delete snap package', name='delete')
285+ def delete_action(self, action, data):
286+ owner = self.context.owner
287+ self.context.destroySelf()
288+ # XXX cjwatson 2015-07-17: This should go to Person:+snaps or
289+ # similar (or something on SnapSet?) once that exists.
290+ self.next_url = canonical_url(owner)
291
292=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
293--- lib/lp/snappy/browser/tests/test_snap.py 2015-08-27 16:06:41 +0000
294+++ lib/lp/snappy/browser/tests/test_snap.py 2015-09-07 16:06:24 +0000
295@@ -10,18 +10,31 @@
296 timedelta,
297 )
298
299+from fixtures import FakeLogger
300+from mechanize import LinkNotFoundError
301 import pytz
302 from zope.component import getUtility
303+from zope.publisher.interfaces import NotFound
304+from zope.security.interfaces import Unauthorized
305
306 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
307 from lp.buildmaster.enums import BuildStatus
308 from lp.buildmaster.interfaces.processor import IProcessorSet
309+from lp.registry.interfaces.series import SeriesStatus
310+from lp.services.database.constants import UTC_NOW
311 from lp.services.features.testing import FeatureFixture
312 from lp.services.webapp import canonical_url
313-from lp.snappy.browser.snap import SnapView
314+from lp.services.webapp.servers import LaunchpadTestRequest
315+from lp.snappy.browser.snap import (
316+ SnapAdminView,
317+ SnapEditView,
318+ SnapView,
319+ )
320 from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
321 from lp.testing import (
322 BrowserTestCase,
323+ login,
324+ login_person,
325 person_logged_in,
326 TestCaseWithFactory,
327 time_counter,
328@@ -30,6 +43,15 @@
329 DatabaseFunctionalLayer,
330 LaunchpadFunctionalLayer,
331 )
332+from lp.testing.matchers import (
333+ MatchesPickerText,
334+ MatchesTagText,
335+ )
336+from lp.testing.pages import (
337+ extract_text,
338+ find_main_content,
339+ find_tags_by_class,
340+ )
341 from lp.testing.publication import test_traverse
342
343
344@@ -55,6 +77,184 @@
345 self.assertEqual(snap, obj)
346
347
348+class TestSnapAdminView(BrowserTestCase):
349+
350+ layer = DatabaseFunctionalLayer
351+
352+ def setUp(self):
353+ super(TestSnapAdminView, self).setUp()
354+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
355+ self.useFixture(FakeLogger())
356+ self.person = self.factory.makePerson(
357+ name="test-person", displayname="Test Person")
358+
359+ def test_unauthorized(self):
360+ # A non-admin user cannot administer a snap package.
361+ login_person(self.person)
362+ snap = self.factory.makeSnap(registrant=self.person)
363+ snap_url = canonical_url(snap)
364+ browser = self.getViewBrowser(snap, user=self.person)
365+ self.assertRaises(
366+ LinkNotFoundError, browser.getLink, "Administer snap package")
367+ self.assertRaises(
368+ Unauthorized, self.getUserBrowser, snap_url + "/+admin",
369+ user=self.person)
370+
371+ def test_admin_snap(self):
372+ # Admins can change require_virtualized.
373+ login("admin@canonical.com")
374+ commercial_admin = self.factory.makePerson(
375+ member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
376+ login_person(self.person)
377+ snap = self.factory.makeSnap(registrant=self.person)
378+ self.assertTrue(snap.require_virtualized)
379+ browser = self.getViewBrowser(snap, user=commercial_admin)
380+ browser.getLink("Administer snap package").click()
381+ browser.getControl("Require virtualized builders").selected = False
382+ browser.getControl("Update snap package").click()
383+ login_person(self.person)
384+ self.assertFalse(snap.require_virtualized)
385+
386+ def test_admin_snap_sets_date_last_modified(self):
387+ # Administering a snap package sets the date_last_modified property.
388+ login("admin@canonical.com")
389+ commercial_admin = self.factory.makePerson(
390+ member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
391+ login_person(self.person)
392+ date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
393+ snap = self.factory.makeSnap(
394+ registrant=self.person, date_created=date_created)
395+ login_person(commercial_admin)
396+ view = SnapAdminView(snap, LaunchpadTestRequest())
397+ view.initialize()
398+ view.request_action.success({"require_virtualized": False})
399+ self.assertSqlAttributeEqualsDate(snap, "date_last_modified", UTC_NOW)
400+
401+
402+class TestSnapEditView(BrowserTestCase):
403+
404+ layer = DatabaseFunctionalLayer
405+
406+ def setUp(self):
407+ super(TestSnapEditView, self).setUp()
408+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
409+ self.useFixture(FakeLogger())
410+ self.person = self.factory.makePerson(
411+ name="test-person", displayname="Test Person")
412+
413+ def test_edit_snap(self):
414+ archive = self.factory.makeArchive()
415+ old_series = self.factory.makeDistroSeries(
416+ distribution=archive.distribution, status=SeriesStatus.CURRENT)
417+ old_branch = self.factory.makeAnyBranch()
418+ snap = self.factory.makeSnap(
419+ registrant=self.person, owner=self.person, distroseries=old_series,
420+ branch=old_branch)
421+ self.factory.makeTeam(
422+ name="new-team", displayname="New Team", members=[self.person])
423+ new_series = self.factory.makeDistroSeries(
424+ distribution=archive.distribution, status=SeriesStatus.DEVELOPMENT)
425+ [new_git_ref] = self.factory.makeGitRefs()
426+
427+ browser = self.getViewBrowser(snap, user=self.person)
428+ browser.getLink("Edit snap package").click()
429+ browser.getControl("Owner").value = ["new-team"]
430+ browser.getControl("Name").value = "new-name"
431+ browser.getControl(name="field.distro_series").value = [
432+ str(new_series.id)]
433+ browser.getControl("Git", index=0).click()
434+ browser.getControl("Git repository").value = (
435+ new_git_ref.repository.identity)
436+ browser.getControl("Git branch path").value = new_git_ref.path
437+ browser.getControl("Update snap package").click()
438+
439+ content = find_main_content(browser.contents)
440+ self.assertEqual("new-name", extract_text(content.h1))
441+ self.assertThat("New Team", MatchesPickerText(content, "edit-owner"))
442+ self.assertThat(
443+ "Distribution series:\n%s\nEdit snap package" %
444+ new_series.fullseriesname,
445+ MatchesTagText(content, "distro_series"))
446+ self.assertThat(
447+ "Source:\n%s\nEdit snap package" % new_git_ref.display_name,
448+ MatchesTagText(content, "source"))
449+
450+ def test_edit_snap_sets_date_last_modified(self):
451+ # Editing a snap package sets the date_last_modified property.
452+ date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
453+ snap = self.factory.makeSnap(
454+ registrant=self.person, date_created=date_created)
455+ with person_logged_in(self.person):
456+ view = SnapEditView(snap, LaunchpadTestRequest())
457+ view.initialize()
458+ view.request_action.success({
459+ "owner": snap.owner,
460+ "name": u"changed",
461+ "distro_series": snap.distro_series,
462+ })
463+ self.assertSqlAttributeEqualsDate(snap, "date_last_modified", UTC_NOW)
464+
465+ def test_edit_snap_already_exists(self):
466+ snap = self.factory.makeSnap(
467+ registrant=self.person, owner=self.person, name=u"one")
468+ self.factory.makeSnap(
469+ registrant=self.person, owner=self.person, name=u"two")
470+ browser = self.getViewBrowser(snap, user=self.person)
471+ browser.getLink("Edit snap package").click()
472+ browser.getControl("Name").value = "two"
473+ browser.getControl("Update snap package").click()
474+ self.assertEqual(
475+ "There is already a snap package owned by Test Person with this "
476+ "name.",
477+ extract_text(find_tags_by_class(browser.contents, "message")[1]))
478+
479+
480+class TestSnapDeleteView(BrowserTestCase):
481+
482+ layer = LaunchpadFunctionalLayer
483+
484+ def setUp(self):
485+ super(TestSnapDeleteView, self).setUp()
486+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
487+ self.person = self.factory.makePerson(
488+ name="test-person", displayname="Test Person")
489+
490+ def test_unauthorized(self):
491+ # A user without edit access cannot delete a snap package.
492+ self.useFixture(FakeLogger())
493+ snap = self.factory.makeSnap(registrant=self.person, owner=self.person)
494+ snap_url = canonical_url(snap)
495+ other_person = self.factory.makePerson()
496+ browser = self.getViewBrowser(snap, user=other_person)
497+ self.assertRaises(
498+ LinkNotFoundError, browser.getLink, "Delete snap package")
499+ self.assertRaises(
500+ Unauthorized, self.getUserBrowser, snap_url + "/+delete",
501+ user=other_person)
502+
503+ def test_delete_snap_without_builds(self):
504+ # A snap package without builds can be deleted.
505+ self.useFixture(FakeLogger())
506+ snap = self.factory.makeSnap(registrant=self.person, owner=self.person)
507+ snap_url = canonical_url(snap)
508+ owner_url = canonical_url(self.person)
509+ browser = self.getViewBrowser(snap, user=self.person)
510+ browser.getLink("Delete snap package").click()
511+ browser.getControl("Delete snap package").click()
512+ self.assertEqual(owner_url, browser.url)
513+ self.assertRaises(NotFound, browser.open, snap_url)
514+
515+ def test_delete_snap_with_builds(self):
516+ # A snap package with builds cannot be deleted.
517+ snap = self.factory.makeSnap(registrant=self.person, owner=self.person)
518+ self.factory.makeSnapBuild(snap=snap)
519+ browser = self.getViewBrowser(snap, user=self.person)
520+ browser.getLink("Delete snap package").click()
521+ self.assertIn("This snap package cannot be deleted", browser.contents)
522+ self.assertRaises(
523+ LookupError, browser.getControl, "Delete snap package")
524+
525+
526 class TestSnapView(BrowserTestCase):
527
528 layer = LaunchpadFunctionalLayer
529
530=== added directory 'lib/lp/snappy/javascript'
531=== added file 'lib/lp/snappy/javascript/snap.edit.js'
532--- lib/lp/snappy/javascript/snap.edit.js 1970-01-01 00:00:00 +0000
533+++ lib/lp/snappy/javascript/snap.edit.js 2015-09-07 16:06:24 +0000
534@@ -0,0 +1,36 @@
535+/* Copyright 2015 Canonical Ltd. This software is licensed under the
536+ * GNU Affero General Public License version 3 (see the file LICENSE).
537+ *
538+ * Control enabling/disabling form elements on the Snap:+edit page.
539+ *
540+ * @module Y.lp.snappy.snap.edit
541+ * @requires node, DOM
542+ */
543+YUI.add('lp.snappy.snap.edit', function(Y) {
544+ Y.log('loading lp.snappy.snap.edit');
545+ var module = Y.namespace('lp.snappy.snap.edit');
546+
547+ module.set_enabled = function(field_id, is_enabled) {
548+ var field = Y.DOM.byId(field_id);
549+ field.disabled = !is_enabled;
550+ };
551+
552+ module.onclick_vcs = function(e) {
553+ var selected_vcs = null;
554+ Y.all('input[name="field.vcs"]').each(function(node) {
555+ if (node.get('checked')) {
556+ selected_vcs = node.get('value');
557+ }
558+ });
559+ module.set_enabled('field.branch', selected_vcs === 'BZR');
560+ module.set_enabled('field.git_repository', selected_vcs === 'GIT');
561+ module.set_enabled('field.git_path', selected_vcs === 'GIT');
562+ };
563+
564+ module.setup = function() {
565+ Y.all('input[name="field.vcs"]').on('click', module.onclick_vcs);
566+
567+ // Set the initial state.
568+ module.onclick_vcs();
569+ };
570+}, '0.1', {'requires': ['node', 'DOM']});
571
572=== added directory 'lib/lp/snappy/javascript/tests'
573=== added file 'lib/lp/snappy/javascript/tests/test_snap.edit.html'
574--- lib/lp/snappy/javascript/tests/test_snap.edit.html 1970-01-01 00:00:00 +0000
575+++ lib/lp/snappy/javascript/tests/test_snap.edit.html 2015-09-07 16:06:24 +0000
576@@ -0,0 +1,146 @@
577+<!DOCTYPE html>
578+<!--
579+Copyright 2015 Canonical Ltd. This software is licensed under the
580+GNU Affero General Public License version 3 (see the file LICENSE).
581+-->
582+
583+<html>
584+ <head>
585+ <title>snappy.snap.edit Tests</title>
586+
587+ <!-- YUI and test setup -->
588+ <script type="text/javascript"
589+ src="../../../../../build/js/yui/yui/yui.js">
590+ </script>
591+ <link rel="stylesheet"
592+ href="../../../../../build/js/yui/console/assets/console-core.css" />
593+ <link rel="stylesheet"
594+ href="../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
595+ <link rel="stylesheet"
596+ href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
597+
598+ <script type="text/javascript"
599+ src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
600+
601+ <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
602+
603+ <!-- Dependencies -->
604+ <script type="text/javascript"
605+ src="../../../../../build/js/lp/app/lp.js"></script>
606+
607+ <!-- The module under test. -->
608+ <script type="text/javascript" src="../snap.edit.js"></script>
609+
610+ <!-- The test suite -->
611+ <script type="text/javascript" src="test_snap.edit.js"></script>
612+
613+ <script type="text/javascript">
614+ YUI().use('lp.snappy.snap.edit', function(Y) {
615+ Y.on('domready', Y.lp.snappy.snap.edit.setup);
616+ });
617+ </script>
618+
619+ </head>
620+ <body class="yui3-skin-sam">
621+ <ul id="suites">
622+ <li>lp.snappy.snap.edit.test</li>
623+ </ul>
624+ <div id="snap.edit">
625+ <form action="." name="launchpadform" method="post"
626+ enctype="multipart/form-data"
627+ accept-charset="UTF-8">
628+
629+ <table class="form">
630+ <tr>
631+ <td colspan="2">
632+ <div>
633+ <label for="field.vcs">Source:</label>
634+ <table>
635+ <tr>
636+ <td>
637+ <label>
638+ <input class="radioType"
639+ id="field.vcs.Bazaar"
640+ name="field.vcs"
641+ type="radio"
642+ value="BZR" />
643+ Bazaar
644+ </label>
645+ <table class="subordinate">
646+ <tr>
647+ <td colspan="2">
648+ <div>
649+ <label for="field.branch">Bazaar branch:</label>
650+ <div>
651+ <input type="text"
652+ value=""
653+ id="field.branch"
654+ name="field.branch"
655+ size="35"
656+ class="" />
657+ </div>
658+ </div>
659+ </td>
660+ </tr>
661+ </table>
662+ </td>
663+ </tr>
664+ <tr>
665+ <td>
666+ <label>
667+ <input class="radioType"
668+ id="field.vcs.Git"
669+ name="field.vcs"
670+ type="radio"
671+ value="GIT" />
672+ Git
673+ </label>
674+ <table class="subordinate">
675+ <tr>
676+ <td colspan="2">
677+ <div>
678+ <label for="field.git_repository">Git repository:</label>
679+ <div>
680+ <input type="text"
681+ value=""
682+ id="field.git_repository"
683+ name="field.git_repository"
684+ size="20"
685+ class="" />
686+ </div>
687+ </div>
688+ </td>
689+ </tr>
690+ <tr>
691+ <td colspan="2">
692+ <div>
693+ <label for="field.git_path">Git path:</label>
694+ <div>
695+ <input class="textType"
696+ id="field.git_path"
697+ name="field.git_path"
698+ size="20"
699+ type="text"
700+ value="" />
701+ </div>
702+ </div>
703+ </td>
704+ </tr>
705+ </table>
706+ </td>
707+ </tr>
708+ </table>
709+ </div>
710+ </td>
711+ </tr>
712+ </table>
713+
714+ <input type="submit" id="field.actions.update"
715+ name="field.actions.update" value="Update snap package"
716+ class="button" />
717+ or&nbsp;
718+ <a href="https://launchpad.dev/~me/+snap/s">Cancel</a>
719+ </form>
720+ </div>
721+ </body>
722+</html>
723
724=== added file 'lib/lp/snappy/javascript/tests/test_snap.edit.js'
725--- lib/lp/snappy/javascript/tests/test_snap.edit.js 1970-01-01 00:00:00 +0000
726+++ lib/lp/snappy/javascript/tests/test_snap.edit.js 2015-09-07 16:06:24 +0000
727@@ -0,0 +1,80 @@
728+/* Copyright 2015 Canonical Ltd. This software is licensed under the
729+ * GNU Affero General Public License version 3 (see the file LICENSE).
730+ *
731+ * Test driver for snap.edit.js.
732+ */
733+YUI.add('lp.snappy.snap.edit.test', function(Y) {
734+ var tests = Y.namespace('lp.snappy.snap.edit.test');
735+ var module = Y.lp.snappy.snap.edit;
736+ tests.suite = new Y.Test.Suite('snappy.snap.edit Tests');
737+
738+ tests.suite.add(new Y.Test.Case({
739+ name: 'snappy.snap.edit_tests',
740+
741+ setUp: function() {
742+ this.tbody = Y.one('#snap.edit');
743+
744+ // Get the individual VCS type radio buttons.
745+ this.vcs_bzr = Y.DOM.byId('field.vcs.Bazaar');
746+ this.vcs_git = Y.DOM.byId('field.vcs.Git');
747+
748+ // Get the input widgets.
749+ this.input_branch = Y.DOM.byId('field.branch');
750+ this.input_git_repository = Y.DOM.byId('field.git_repository');
751+ this.input_git_path = Y.DOM.byId('field.git_path');
752+ },
753+
754+ tearDown: function() {
755+ delete this.tbody;
756+ },
757+
758+ test_handlers_connected: function() {
759+ // Manually invoke the setup function to ensure the handlers are
760+ // set.
761+ module.setup();
762+
763+ var check_handler = function(field, expected) {
764+ var custom_events = Y.Event.getListeners(field, 'click');
765+ var click_event = custom_events[0];
766+ var subscribers = click_event.subscribers;
767+ Y.each(subscribers, function(sub) {
768+ Y.Assert.isTrue(sub.contains(expected),
769+ 'handler not set up');
770+ });
771+ };
772+
773+ check_handler(this.vcs_bzr, module.onclick_vcs);
774+ check_handler(this.vcs_git, module.onclick_vcs);
775+ },
776+
777+ test_select_vcs_bzr: function() {
778+ this.vcs_bzr.checked = true;
779+ module.onclick_vcs();
780+ // The branch input field is enabled.
781+ Y.Assert.isFalse(this.input_branch.disabled,
782+ 'branch field disabled');
783+ // The git_repository and git_path input fields are disabled.
784+ Y.Assert.isTrue(this.input_git_repository.disabled,
785+ 'git_repository field not disabled');
786+ Y.Assert.isTrue(this.input_git_path.disabled,
787+ 'git_path field not disabled');
788+ },
789+
790+ test_select_vcs_git: function() {
791+ this.vcs_git.checked = true;
792+ module.onclick_vcs();
793+ // The branch input field is disabled.
794+ Y.Assert.isTrue(this.input_branch.disabled,
795+ 'branch field not disabled');
796+ // The git_repository and git_path input fields are enabled.
797+ Y.Assert.isFalse(this.input_git_repository.disabled,
798+ 'git_repository field disabled');
799+ Y.Assert.isFalse(this.input_git_path.disabled,
800+ 'git_path field disabled');
801+ }
802+ }));
803+}, '0.1', {
804+ requires: ['lp.testing.runner', 'test', 'test-console',
805+ 'Event', 'node-event-simulate',
806+ 'lp.snappy.snap.edit']
807+});
808
809=== added file 'lib/lp/snappy/templates/snap-delete.pt'
810--- lib/lp/snappy/templates/snap-delete.pt 1970-01-01 00:00:00 +0000
811+++ lib/lp/snappy/templates/snap-delete.pt 2015-09-07 16:06:24 +0000
812@@ -0,0 +1,21 @@
813+<html
814+ xmlns="http://www.w3.org/1999/xhtml"
815+ xmlns:tal="http://xml.zope.org/namespaces/tal"
816+ xmlns:metal="http://xml.zope.org/namespaces/metal"
817+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
818+ metal:use-macro="view/macro:page/main_only"
819+ i18n:domain="launchpad">
820+<body>
821+
822+ <div metal:fill-slot="main">
823+ <tal:has-builds condition="view/has_builds">
824+ This snap package cannot be deleted as builds have been created for it.
825+ </tal:has-builds>
826+
827+ <tal:has-no-builds condition="not: view/has_builds">
828+ <div metal:use-macro="context/@@launchpad_form/form"/>
829+ </tal:has-no-builds>
830+ </div>
831+
832+</body>
833+</html>
834
835=== added file 'lib/lp/snappy/templates/snap-edit.pt'
836--- lib/lp/snappy/templates/snap-edit.pt 1970-01-01 00:00:00 +0000
837+++ lib/lp/snappy/templates/snap-edit.pt 2015-09-07 16:06:24 +0000
838@@ -0,0 +1,79 @@
839+<html
840+ xmlns="http://www.w3.org/1999/xhtml"
841+ xmlns:tal="http://xml.zope.org/namespaces/tal"
842+ xmlns:metal="http://xml.zope.org/namespaces/metal"
843+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
844+ metal:use-macro="view/macro:page/main_only"
845+ i18n:domain="launchpad">
846+<body>
847+
848+<metal:block fill-slot="head_epilogue">
849+ <style type="text/css">
850+ .subordinate {
851+ margin: 0.5em 0 0.5em 4em;
852+ }
853+ </style>
854+</metal:block>
855+
856+<div metal:fill-slot="main">
857+ <div metal:use-macro="context/@@launchpad_form/form">
858+ <metal:formbody fill-slot="widgets">
859+ <table class="form">
860+ <tal:widget define="widget nocall:view/widgets/owner">
861+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
862+ </tal:widget>
863+ <tal:widget define="widget nocall:view/widgets/name">
864+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
865+ </tal:widget>
866+ <tal:widget define="widget nocall:view/widgets/distro_series">
867+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
868+ </tal:widget>
869+
870+ <tr>
871+ <td>
872+ <div>
873+ <label for="field.vcs">Source:</label>
874+ <table>
875+ <tr>
876+ <td>
877+ <label tal:replace="structure view/vcs_bzr_radio" />
878+ <table class="subordinate">
879+ <tal:widget define="widget nocall:view/widgets/branch">
880+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
881+ </tal:widget>
882+ </table>
883+ </td>
884+ </tr>
885+
886+ <tr>
887+ <td>
888+ <label tal:replace="structure view/vcs_git_radio" />
889+ <table class="subordinate">
890+ <tal:widget define="widget nocall:view/widgets/git_repository">
891+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
892+ </tal:widget>
893+ <tal:widget define="widget nocall:view/widgets/git_path">
894+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
895+ </tal:widget>
896+ </table>
897+ </td>
898+ </tr>
899+ </table>
900+ </div>
901+ </td>
902+ </tr>
903+ </table>
904+ </metal:formbody>
905+ </div>
906+
907+ <script type="text/javascript">
908+ LPJS.use('lp.snappy.snap.edit', function(Y) {
909+ Y.on('domready', function(e) {
910+ Y.lp.snappy.snap.edit.setup();
911+ }, window);
912+ });
913+ </script>
914+</div>
915+
916+</body>
917+</html>
918
919=== modified file 'lib/lp/snappy/templates/snap-index.pt'
920--- lib/lp/snappy/templates/snap-index.pt 2015-08-27 16:06:41 +0000
921+++ lib/lp/snappy/templates/snap-index.pt 2015-09-07 16:06:24 +0000
922@@ -30,18 +30,22 @@
923 <div class="two-column-list">
924 <dl id="owner">
925 <dt>Owner:</dt>
926- <dd tal:content="structure context/owner/fmt:link"/>
927+ <dd tal:content="structure view/person_picker"/>
928 </dl>
929 <dl id="distro_series">
930 <dt>Distribution series:</dt>
931 <dd tal:define="distro_series context/distro_series">
932 <a tal:attributes="href distro_series/fmt:url"
933 tal:content="distro_series/fullseriesname"/>
934+ <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
935 </dd>
936 </dl>
937 <dl id="source" tal:define="source view/source" tal:condition="source">
938 <dt>Source:</dt>
939- <dd tal:content="structure source/fmt:link"/>
940+ <dd>
941+ <a tal:replace="structure source/fmt:link"/>
942+ <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
943+ </dd>
944 </dl>
945 </div>
946
947
948=== added file 'lib/lp/snappy/tests/test_yuitests.py'
949--- lib/lp/snappy/tests/test_yuitests.py 1970-01-01 00:00:00 +0000
950+++ lib/lp/snappy/tests/test_yuitests.py 2015-09-07 16:06:24 +0000
951@@ -0,0 +1,24 @@
952+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
953+# GNU Affero General Public License version 3 (see the file LICENSE).
954+
955+"""Run YUI.test tests."""
956+
957+__metaclass__ = type
958+__all__ = []
959+
960+from lp.testing import (
961+ build_yui_unittest_suite,
962+ YUIUnitTestCase,
963+ )
964+from lp.testing.layers import YUITestLayer
965+
966+
967+class SnappyYUIUnitTestCase(YUIUnitTestCase):
968+
969+ layer = YUITestLayer
970+ suite_name = 'SnappyYUIUnitTests'
971+
972+
973+def test_suite():
974+ app_testing_path = 'lp/snappy'
975+ return build_yui_unittest_suite(app_testing_path, SnappyYUIUnitTestCase)