Merge lp:~cjwatson/launchpad/snap-edit-views into lp:launchpad
- snap-edit-views
- Merge into devel
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 | ||||
Related bugs: |
|
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 |
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) |