Merge ~cjwatson/launchpad:charm-recipe-build-basic-browser into launchpad:master
- Git
- lp:~cjwatson/launchpad
- charm-recipe-build-basic-browser
- Merge into master
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | 4a5561d2fefbcacc3b3137ff7a44ea2ef5ed6404 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~cjwatson/launchpad:charm-recipe-build-basic-browser |
Merge into: | launchpad:master |
Prerequisite: | ~cjwatson/launchpad:charm-recipe-basic-browser |
Diff against target: |
786 lines (+722/-0) 7 files modified
lib/lp/app/browser/configure.zcml (+6/-0) lib/lp/app/browser/tales.py (+12/-0) lib/lp/charms/browser/charmrecipebuild.py (+171/-0) lib/lp/charms/browser/configure.zcml (+40/-0) lib/lp/charms/browser/tests/test_charmrecipebuild.py (+264/-0) lib/lp/charms/templates/charmrecipebuild-index.pt (+201/-0) lib/lp/charms/templates/charmrecipebuild-retry.pt (+28/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Review via email: mp+403791@code.launchpad.net |
Commit message
Add basic charm recipe build views
Description of the change
To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/app/browser/configure.zcml b/lib/lp/app/browser/configure.zcml |
2 | index b08d380..005968c 100644 |
3 | --- a/lib/lp/app/browser/configure.zcml |
4 | +++ b/lib/lp/app/browser/configure.zcml |
5 | @@ -877,6 +877,12 @@ |
6 | name="fmt" |
7 | /> |
8 | <adapter |
9 | + for="lp.charms.interfaces.charmrecipe.ICharmRecipe" |
10 | + provides="zope.traversing.interfaces.IPathAdapter" |
11 | + factory="lp.app.browser.tales.CharmRecipeFormatterAPI" |
12 | + name="fmt" |
13 | + /> |
14 | + <adapter |
15 | for="lp.blueprints.interfaces.specification.ISpecification" |
16 | provides="zope.traversing.interfaces.IPathAdapter" |
17 | factory="lp.app.browser.tales.SpecificationFormatterAPI" |
18 | diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py |
19 | index 17fd33f..1bf84aa 100644 |
20 | --- a/lib/lp/app/browser/tales.py |
21 | +++ b/lib/lp/app/browser/tales.py |
22 | @@ -1951,6 +1951,18 @@ class SnappySeriesFormatterAPI(CustomizableFormatter): |
23 | return {'title': self._context.title} |
24 | |
25 | |
26 | +class CharmRecipeFormatterAPI(CustomizableFormatter): |
27 | + """Adapter providing fmt support for ICharmRecipe objects.""" |
28 | + |
29 | + _link_summary_template = _( |
30 | + 'Charm recipe %(name)s for %(owner)s in %(project)s') |
31 | + |
32 | + def _link_summary_values(self): |
33 | + return {'name': self._context.name, |
34 | + 'owner': self._context.owner.displayname, |
35 | + 'project': self._context.project.displayname} |
36 | + |
37 | + |
38 | class SpecificationFormatterAPI(CustomizableFormatter): |
39 | """Adapter providing fmt support for Specification objects""" |
40 | |
41 | diff --git a/lib/lp/charms/browser/charmrecipebuild.py b/lib/lp/charms/browser/charmrecipebuild.py |
42 | new file mode 100644 |
43 | index 0000000..f695084 |
44 | --- /dev/null |
45 | +++ b/lib/lp/charms/browser/charmrecipebuild.py |
46 | @@ -0,0 +1,171 @@ |
47 | +# Copyright 2021 Canonical Ltd. This software is licensed under the |
48 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
49 | + |
50 | +"""Charm recipe build views.""" |
51 | + |
52 | +from __future__ import absolute_import, print_function, unicode_literals |
53 | + |
54 | +__metaclass__ = type |
55 | +__all__ = [ |
56 | + "CharmRecipeBuildContextMenu", |
57 | + "CharmRecipeBuildNavigation", |
58 | + "CharmRecipeBuildView", |
59 | + ] |
60 | + |
61 | +from zope.interface import Interface |
62 | + |
63 | +from lp.app.browser.launchpadform import ( |
64 | + action, |
65 | + LaunchpadFormView, |
66 | + ) |
67 | +from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild |
68 | +from lp.services.librarian.browser import ( |
69 | + FileNavigationMixin, |
70 | + ProxiedLibraryFileAlias, |
71 | + ) |
72 | +from lp.services.propertycache import cachedproperty |
73 | +from lp.services.webapp import ( |
74 | + canonical_url, |
75 | + ContextMenu, |
76 | + enabled_with_permission, |
77 | + LaunchpadView, |
78 | + Link, |
79 | + Navigation, |
80 | + ) |
81 | +from lp.soyuz.interfaces.binarypackagebuild import IBuildRescoreForm |
82 | + |
83 | + |
84 | +class CharmRecipeBuildNavigation(Navigation, FileNavigationMixin): |
85 | + usedfor = ICharmRecipeBuild |
86 | + |
87 | + |
88 | +class CharmRecipeBuildContextMenu(ContextMenu): |
89 | + """Context menu for charm recipe builds.""" |
90 | + |
91 | + usedfor = ICharmRecipeBuild |
92 | + |
93 | + facet = "overview" |
94 | + |
95 | + links = ("retry", "cancel", "rescore") |
96 | + |
97 | + @enabled_with_permission("launchpad.Edit") |
98 | + def retry(self): |
99 | + return Link( |
100 | + "+retry", "Retry this build", icon="retry", |
101 | + enabled=self.context.can_be_retried) |
102 | + |
103 | + @enabled_with_permission("launchpad.Edit") |
104 | + def cancel(self): |
105 | + return Link( |
106 | + "+cancel", "Cancel build", icon="remove", |
107 | + enabled=self.context.can_be_cancelled) |
108 | + |
109 | + @enabled_with_permission("launchpad.Admin") |
110 | + def rescore(self): |
111 | + return Link( |
112 | + "+rescore", "Rescore build", icon="edit", |
113 | + enabled=self.context.can_be_rescored) |
114 | + |
115 | + |
116 | +class CharmRecipeBuildView(LaunchpadView): |
117 | + """Default view of a charm recipe build.""" |
118 | + |
119 | + @property |
120 | + def label(self): |
121 | + return self.context.title |
122 | + |
123 | + page_title = label |
124 | + |
125 | + @cachedproperty |
126 | + def files(self): |
127 | + """Return `LibraryFileAlias`es for files produced by this build.""" |
128 | + if not self.context.was_built: |
129 | + return None |
130 | + |
131 | + return [ |
132 | + ProxiedLibraryFileAlias(alias, self.context) |
133 | + for _, alias, _ in self.context.getFiles() if not alias.deleted] |
134 | + |
135 | + @cachedproperty |
136 | + def has_files(self): |
137 | + return bool(self.files) |
138 | + |
139 | + @property |
140 | + def next_url(self): |
141 | + return canonical_url(self.context) |
142 | + |
143 | + |
144 | +class CharmRecipeBuildRetryView(LaunchpadFormView): |
145 | + """View for retrying a charm recipe build.""" |
146 | + |
147 | + class schema(Interface): |
148 | + """Schema for retrying a build.""" |
149 | + |
150 | + page_title = label = "Retry build" |
151 | + |
152 | + @property |
153 | + def cancel_url(self): |
154 | + return canonical_url(self.context) |
155 | + next_url = cancel_url |
156 | + |
157 | + @action("Retry build", name="retry") |
158 | + def request_action(self, action, data): |
159 | + """Retry the build.""" |
160 | + if not self.context.can_be_retried: |
161 | + self.request.response.addErrorNotification( |
162 | + "Build cannot be retried") |
163 | + else: |
164 | + self.context.retry() |
165 | + self.request.response.addInfoNotification("Build has been queued") |
166 | + |
167 | + self.request.response.redirect(self.next_url) |
168 | + |
169 | + |
170 | +class CharmRecipeBuildCancelView(LaunchpadFormView): |
171 | + """View for cancelling a charm recipe build.""" |
172 | + |
173 | + class schema(Interface): |
174 | + """Schema for cancelling a build.""" |
175 | + |
176 | + page_title = label = "Cancel build" |
177 | + |
178 | + @property |
179 | + def cancel_url(self): |
180 | + return canonical_url(self.context) |
181 | + next_url = cancel_url |
182 | + |
183 | + @action("Cancel build", name="cancel") |
184 | + def request_action(self, action, data): |
185 | + """Cancel the build.""" |
186 | + self.context.cancel() |
187 | + |
188 | + |
189 | +class CharmRecipeBuildRescoreView(LaunchpadFormView): |
190 | + """View for rescoring a charm recipe build.""" |
191 | + |
192 | + schema = IBuildRescoreForm |
193 | + |
194 | + page_title = label = "Rescore build" |
195 | + |
196 | + def __call__(self): |
197 | + if self.context.can_be_rescored: |
198 | + return super(CharmRecipeBuildRescoreView, self).__call__() |
199 | + self.request.response.addWarningNotification( |
200 | + "Cannot rescore this build because it is not queued.") |
201 | + self.request.response.redirect(canonical_url(self.context)) |
202 | + |
203 | + @property |
204 | + def cancel_url(self): |
205 | + return canonical_url(self.context) |
206 | + next_url = cancel_url |
207 | + |
208 | + @action("Rescore build", name="rescore") |
209 | + def request_action(self, action, data): |
210 | + """Rescore the build.""" |
211 | + score = data.get("priority") |
212 | + self.context.rescore(score) |
213 | + self.request.response.addNotification("Build rescored to %s." % score) |
214 | + |
215 | + @property |
216 | + def initial_values(self): |
217 | + return {"score": str(self.context.buildqueue_record.lastscore)} |
218 | diff --git a/lib/lp/charms/browser/configure.zcml b/lib/lp/charms/browser/configure.zcml |
219 | index 92d78bf..0475288 100644 |
220 | --- a/lib/lp/charms/browser/configure.zcml |
221 | +++ b/lib/lp/charms/browser/configure.zcml |
222 | @@ -28,13 +28,53 @@ |
223 | for="lp.charms.interfaces.charmrecipe.ICharmRecipe" |
224 | factory="lp.charms.browser.charmrecipe.CharmRecipeBreadcrumb" |
225 | permission="zope.Public" /> |
226 | + |
227 | <browser:url |
228 | for="lp.charms.interfaces.charmrecipe.ICharmRecipeBuildRequest" |
229 | path_expression="string:+build-request/${id}" |
230 | attribute_to_parent="recipe" /> |
231 | + |
232 | <browser:url |
233 | for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild" |
234 | path_expression="string:+build/${id}" |
235 | attribute_to_parent="recipe" /> |
236 | + <browser:menus |
237 | + module="lp.charms.browser.charmrecipebuild" |
238 | + classes="CharmRecipeBuildContextMenu" /> |
239 | + <browser:navigation |
240 | + module="lp.charms.browser.charmrecipebuild" |
241 | + classes="CharmRecipeBuildNavigation" /> |
242 | + <browser:defaultView |
243 | + for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild" |
244 | + name="+index" /> |
245 | + <browser:page |
246 | + for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild" |
247 | + class="lp.charms.browser.charmrecipebuild.CharmRecipeBuildView" |
248 | + permission="launchpad.View" |
249 | + name="+index" |
250 | + template="../templates/charmrecipebuild-index.pt" /> |
251 | + <browser:page |
252 | + for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild" |
253 | + class="lp.charms.browser.charmrecipebuild.CharmRecipeBuildRetryView" |
254 | + permission="launchpad.Edit" |
255 | + name="+retry" |
256 | + template="../templates/charmrecipebuild-retry.pt" /> |
257 | + <browser:page |
258 | + for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild" |
259 | + class="lp.charms.browser.charmrecipebuild.CharmRecipeBuildCancelView" |
260 | + permission="launchpad.Edit" |
261 | + name="+cancel" |
262 | + template="../../app/templates/generic-edit.pt" /> |
263 | + <browser:page |
264 | + for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild" |
265 | + class="lp.charms.browser.charmrecipebuild.CharmRecipeBuildRescoreView" |
266 | + permission="launchpad.Admin" |
267 | + name="+rescore" |
268 | + template="../../app/templates/generic-edit.pt" /> |
269 | + <adapter |
270 | + provides="lp.services.webapp.interfaces.IBreadcrumb" |
271 | + for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild" |
272 | + factory="lp.services.webapp.breadcrumb.TitleBreadcrumb" |
273 | + permission="zope.Public" /> |
274 | </facet> |
275 | </configure> |
276 | diff --git a/lib/lp/charms/browser/tests/test_charmrecipebuild.py b/lib/lp/charms/browser/tests/test_charmrecipebuild.py |
277 | new file mode 100644 |
278 | index 0000000..0978a0a |
279 | --- /dev/null |
280 | +++ b/lib/lp/charms/browser/tests/test_charmrecipebuild.py |
281 | @@ -0,0 +1,264 @@ |
282 | +# Copyright 2021 Canonical Ltd. This software is licensed under the |
283 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
284 | + |
285 | +"""Test charm recipe build views.""" |
286 | + |
287 | +from __future__ import absolute_import, print_function, unicode_literals |
288 | + |
289 | +__metaclass__ = type |
290 | + |
291 | +import re |
292 | + |
293 | +from fixtures import FakeLogger |
294 | +import soupmatchers |
295 | +from storm.locals import Store |
296 | +from testtools.matchers import StartsWith |
297 | +import transaction |
298 | +from zope.component import getUtility |
299 | +from zope.security.interfaces import Unauthorized |
300 | +from zope.security.proxy import removeSecurityProxy |
301 | +from zope.testbrowser.browser import LinkNotFoundError |
302 | + |
303 | +from lp.app.enums import InformationType |
304 | +from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
305 | +from lp.buildmaster.enums import BuildStatus |
306 | +from lp.charms.interfaces.charmrecipe import ( |
307 | + CHARM_RECIPE_ALLOW_CREATE, |
308 | + CHARM_RECIPE_PRIVATE_FEATURE_FLAG, |
309 | + ) |
310 | +from lp.services.features.testing import FeatureFixture |
311 | +from lp.services.webapp import canonical_url |
312 | +from lp.testing import ( |
313 | + ANONYMOUS, |
314 | + BrowserTestCase, |
315 | + login, |
316 | + person_logged_in, |
317 | + TestCaseWithFactory, |
318 | + ) |
319 | +from lp.testing.layers import ( |
320 | + DatabaseFunctionalLayer, |
321 | + LaunchpadFunctionalLayer, |
322 | + ) |
323 | +from lp.testing.pages import ( |
324 | + extract_text, |
325 | + find_main_content, |
326 | + find_tags_by_class, |
327 | + ) |
328 | +from lp.testing.views import create_initialized_view |
329 | + |
330 | + |
331 | +class TestCanonicalUrlForCharmRecipeBuild(TestCaseWithFactory): |
332 | + |
333 | + layer = DatabaseFunctionalLayer |
334 | + |
335 | + def setUp(self): |
336 | + super(TestCanonicalUrlForCharmRecipeBuild, self).setUp() |
337 | + self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"})) |
338 | + |
339 | + def test_canonical_url(self): |
340 | + owner = self.factory.makePerson(name="person") |
341 | + project = self.factory.makeProduct(name="charm-project") |
342 | + recipe = self.factory.makeCharmRecipe( |
343 | + registrant=owner, owner=owner, project=project, name="charm") |
344 | + build = self.factory.makeCharmRecipeBuild( |
345 | + requester=owner, recipe=recipe) |
346 | + self.assertThat( |
347 | + canonical_url(build), |
348 | + StartsWith( |
349 | + "http://launchpad.test/~person/charm-project/+charm/charm/" |
350 | + "+build/")) |
351 | + |
352 | + |
353 | +class TestCharmRecipeBuildView(TestCaseWithFactory): |
354 | + |
355 | + layer = LaunchpadFunctionalLayer |
356 | + |
357 | + def setUp(self): |
358 | + super(TestCharmRecipeBuildView, self).setUp() |
359 | + self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"})) |
360 | + |
361 | + def test_files(self): |
362 | + # CharmRecipeBuildView.files returns all the associated files. |
363 | + build = self.factory.makeCharmRecipeBuild( |
364 | + status=BuildStatus.FULLYBUILT) |
365 | + charm_file = self.factory.makeCharmFile(build=build) |
366 | + build_view = create_initialized_view(build, "+index") |
367 | + self.assertEqual( |
368 | + [charm_file.library_file.filename], |
369 | + [lfa.filename for lfa in build_view.files]) |
370 | + # Deleted files won't be included. |
371 | + self.assertFalse(charm_file.library_file.deleted) |
372 | + removeSecurityProxy(charm_file.library_file).content = None |
373 | + self.assertTrue(charm_file.library_file.deleted) |
374 | + build_view = create_initialized_view(build, "+index") |
375 | + self.assertEqual([], build_view.files) |
376 | + |
377 | + def test_revision_id(self): |
378 | + build = self.factory.makeCharmRecipeBuild() |
379 | + build.updateStatus( |
380 | + BuildStatus.FULLYBUILT, slave_status={"revision_id": "dummy"}) |
381 | + build_view = create_initialized_view(build, "+index") |
382 | + self.assertThat(build_view(), soupmatchers.HTMLContains( |
383 | + soupmatchers.Tag( |
384 | + "revision ID", "li", attrs={"id": "revision-id"}, |
385 | + text=re.compile(r"^\s*Revision: dummy\s*$")))) |
386 | + |
387 | + |
388 | +class TestCharmRecipeBuildOperations(BrowserTestCase): |
389 | + |
390 | + layer = DatabaseFunctionalLayer |
391 | + |
392 | + def setUp(self): |
393 | + super(TestCharmRecipeBuildOperations, self).setUp() |
394 | + self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"})) |
395 | + self.useFixture(FakeLogger()) |
396 | + self.build = self.factory.makeCharmRecipeBuild() |
397 | + self.build_url = canonical_url(self.build) |
398 | + self.requester = self.build.requester |
399 | + self.buildd_admin = self.factory.makePerson( |
400 | + member_of=[getUtility(ILaunchpadCelebrities).buildd_admin]) |
401 | + |
402 | + def test_retry_build(self): |
403 | + # The requester of a build can retry it. |
404 | + self.build.updateStatus(BuildStatus.FAILEDTOBUILD) |
405 | + transaction.commit() |
406 | + browser = self.getViewBrowser(self.build, user=self.requester) |
407 | + browser.getLink("Retry this build").click() |
408 | + self.assertEqual(self.build_url, browser.getLink("Cancel").url) |
409 | + browser.getControl("Retry build").click() |
410 | + self.assertEqual(self.build_url, browser.url) |
411 | + login(ANONYMOUS) |
412 | + self.assertEqual(BuildStatus.NEEDSBUILD, self.build.status) |
413 | + |
414 | + def test_retry_build_random_user(self): |
415 | + # An unrelated non-admin user cannot retry a build. |
416 | + self.build.updateStatus(BuildStatus.FAILEDTOBUILD) |
417 | + transaction.commit() |
418 | + user = self.factory.makePerson() |
419 | + browser = self.getViewBrowser(self.build, user=user) |
420 | + self.assertRaises( |
421 | + LinkNotFoundError, browser.getLink, "Retry this build") |
422 | + self.assertRaises( |
423 | + Unauthorized, self.getUserBrowser, self.build_url + "/+retry", |
424 | + user=user) |
425 | + |
426 | + def test_retry_build_wrong_state(self): |
427 | + # If the build isn't in an unsuccessful terminal state, you can't |
428 | + # retry it. |
429 | + self.build.updateStatus(BuildStatus.FULLYBUILT) |
430 | + browser = self.getViewBrowser(self.build, user=self.requester) |
431 | + self.assertRaises( |
432 | + LinkNotFoundError, browser.getLink, "Retry this build") |
433 | + |
434 | + def test_cancel_build(self): |
435 | + # The requester of a build can cancel it. |
436 | + self.build.queueBuild() |
437 | + transaction.commit() |
438 | + browser = self.getViewBrowser(self.build, user=self.requester) |
439 | + browser.getLink("Cancel build").click() |
440 | + self.assertEqual(self.build_url, browser.getLink("Cancel").url) |
441 | + browser.getControl("Cancel build").click() |
442 | + self.assertEqual(self.build_url, browser.url) |
443 | + login(ANONYMOUS) |
444 | + self.assertEqual(BuildStatus.CANCELLED, self.build.status) |
445 | + |
446 | + def test_cancel_build_random_user(self): |
447 | + # An unrelated non-admin user cannot cancel a build. |
448 | + self.build.queueBuild() |
449 | + transaction.commit() |
450 | + user = self.factory.makePerson() |
451 | + browser = self.getViewBrowser(self.build, user=user) |
452 | + self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build") |
453 | + self.assertRaises( |
454 | + Unauthorized, self.getUserBrowser, self.build_url + "/+cancel", |
455 | + user=user) |
456 | + |
457 | + def test_cancel_build_wrong_state(self): |
458 | + # If the build isn't queued, you can't cancel it. |
459 | + browser = self.getViewBrowser(self.build, user=self.requester) |
460 | + self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build") |
461 | + |
462 | + def test_rescore_build(self): |
463 | + # A buildd admin can rescore a build. |
464 | + self.build.queueBuild() |
465 | + transaction.commit() |
466 | + browser = self.getViewBrowser(self.build, user=self.buildd_admin) |
467 | + browser.getLink("Rescore build").click() |
468 | + self.assertEqual(self.build_url, browser.getLink("Cancel").url) |
469 | + browser.getControl("Priority").value = "1024" |
470 | + browser.getControl("Rescore build").click() |
471 | + self.assertEqual(self.build_url, browser.url) |
472 | + login(ANONYMOUS) |
473 | + self.assertEqual(1024, self.build.buildqueue_record.lastscore) |
474 | + |
475 | + def test_rescore_build_invalid_score(self): |
476 | + # Build scores can only take numbers. |
477 | + self.build.queueBuild() |
478 | + transaction.commit() |
479 | + browser = self.getViewBrowser(self.build, user=self.buildd_admin) |
480 | + browser.getLink("Rescore build").click() |
481 | + self.assertEqual(self.build_url, browser.getLink("Cancel").url) |
482 | + browser.getControl("Priority").value = "tentwentyfour" |
483 | + browser.getControl("Rescore build").click() |
484 | + self.assertEqual( |
485 | + "Invalid integer data", |
486 | + extract_text(find_tags_by_class(browser.contents, "message")[1])) |
487 | + |
488 | + def test_rescore_build_not_admin(self): |
489 | + # A non-admin user cannot cancel a build. |
490 | + self.build.queueBuild() |
491 | + transaction.commit() |
492 | + user = self.factory.makePerson() |
493 | + browser = self.getViewBrowser(self.build, user=user) |
494 | + self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build") |
495 | + self.assertRaises( |
496 | + Unauthorized, self.getUserBrowser, self.build_url + "/+rescore", |
497 | + user=user) |
498 | + |
499 | + def test_rescore_build_wrong_state(self): |
500 | + # If the build isn't NEEDSBUILD, you can't rescore it. |
501 | + self.build.queueBuild() |
502 | + with person_logged_in(self.requester): |
503 | + self.build.cancel() |
504 | + browser = self.getViewBrowser(self.build, user=self.buildd_admin) |
505 | + self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build") |
506 | + |
507 | + def test_rescore_build_wrong_state_stale_link(self): |
508 | + # An attempt to rescore a non-queued build from a stale link shows a |
509 | + # sensible error message. |
510 | + self.build.queueBuild() |
511 | + with person_logged_in(self.requester): |
512 | + self.build.cancel() |
513 | + browser = self.getViewBrowser( |
514 | + self.build, "+rescore", user=self.buildd_admin) |
515 | + self.assertEqual(self.build_url, browser.url) |
516 | + self.assertThat(browser.contents, soupmatchers.HTMLContains( |
517 | + soupmatchers.Tag( |
518 | + "notification", "div", attrs={"class": "warning message"}, |
519 | + text="Cannot rescore this build because it is not queued."))) |
520 | + |
521 | + def test_builder_history(self): |
522 | + Store.of(self.build).flush() |
523 | + self.build.updateStatus( |
524 | + BuildStatus.FULLYBUILT, builder=self.factory.makeBuilder()) |
525 | + title = self.build.title |
526 | + browser = self.getViewBrowser(self.build.builder, "+history") |
527 | + self.assertTextMatchesExpressionIgnoreWhitespace( |
528 | + r"Build history.*%s" % re.escape(title), |
529 | + extract_text(find_main_content(browser.contents))) |
530 | + self.assertEqual(self.build_url, browser.getLink(title).url) |
531 | + |
532 | + def makeBuildingRecipe(self, information_type=InformationType.PUBLIC): |
533 | + builder = self.factory.makeBuilder() |
534 | + build = self.factory.makeCharmRecipeBuild( |
535 | + information_type=information_type) |
536 | + build.updateStatus(BuildStatus.BUILDING, builder=builder) |
537 | + build.queueBuild() |
538 | + build.buildqueue_record.builder = builder |
539 | + build.buildqueue_record.logtail = "tail of the log" |
540 | + return build |
541 | + |
542 | + def test_builder_index_public(self): |
543 | + build = self.makeBuildingRecipe() |
544 | + browser = self.getViewBrowser(build.builder, no_login=True) |
545 | + self.assertIn("tail of the log", browser.contents) |
546 | diff --git a/lib/lp/charms/templates/charmrecipebuild-index.pt b/lib/lp/charms/templates/charmrecipebuild-index.pt |
547 | new file mode 100644 |
548 | index 0000000..1d0e4f0 |
549 | --- /dev/null |
550 | +++ b/lib/lp/charms/templates/charmrecipebuild-index.pt |
551 | @@ -0,0 +1,201 @@ |
552 | +<html |
553 | + xmlns="http://www.w3.org/1999/xhtml" |
554 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
555 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
556 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
557 | + metal:use-macro="view/macro:page/main_only" |
558 | + i18n:domain="launchpad" |
559 | +> |
560 | + |
561 | + <body> |
562 | + |
563 | + <tal:registering metal:fill-slot="registering"> |
564 | + created |
565 | + <span tal:content="context/date_created/fmt:displaydate" |
566 | + tal:attributes="title context/date_created/fmt:datetime"/> |
567 | + </tal:registering> |
568 | + |
569 | + <div metal:fill-slot="main"> |
570 | + |
571 | + <div class="yui-g"> |
572 | + |
573 | + <div id="status" class="yui-u first"> |
574 | + <div class="portlet"> |
575 | + <div metal:use-macro="template/macros/status"/> |
576 | + </div> |
577 | + </div> |
578 | + |
579 | + <div id="details" class="yui-u"> |
580 | + <div class="portlet"> |
581 | + <div metal:use-macro="template/macros/details"/> |
582 | + </div> |
583 | + </div> |
584 | + |
585 | + </div> <!-- yui-g --> |
586 | + |
587 | + <div id="files" class="portlet" tal:condition="view/has_files"> |
588 | + <div metal:use-macro="template/macros/files"/> |
589 | + </div> |
590 | + |
591 | + <div id="buildlog" class="portlet" |
592 | + tal:condition="context/status/enumvalue:BUILDING"> |
593 | + <div metal:use-macro="template/macros/buildlog"/> |
594 | + </div> |
595 | + |
596 | + </div> <!-- main --> |
597 | + |
598 | + |
599 | +<metal:macros fill-slot="bogus"> |
600 | + |
601 | + <metal:macro define-macro="details"> |
602 | + <tal:comment replace="nothing"> |
603 | + Details section. |
604 | + </tal:comment> |
605 | + <h2>Build details</h2> |
606 | + <div class="two-column-list"> |
607 | + <dl> |
608 | + <dt>Recipe:</dt> |
609 | + <dd> |
610 | + <tal:recipe replace="structure context/recipe/fmt:link"/> |
611 | + </dd> |
612 | + </dl> |
613 | + <dl> |
614 | + <dt>Series:</dt> |
615 | + <dd><a class="sprite distribution" |
616 | + tal:define="series context/distro_series" |
617 | + tal:attributes="href series/fmt:url" |
618 | + tal:content="series/displayname"/> |
619 | + </dd> |
620 | + </dl> |
621 | + <dl> |
622 | + <dt>Architecture:</dt> |
623 | + <dd><a class="sprite distribution" |
624 | + tal:define="archseries context/distro_arch_series" |
625 | + tal:attributes="href archseries/fmt:url" |
626 | + tal:content="archseries/architecturetag"/> |
627 | + </dd> |
628 | + </dl> |
629 | + </div> |
630 | + </metal:macro> |
631 | + |
632 | + <metal:macro define-macro="status"> |
633 | + <tal:comment replace="nothing"> |
634 | + Status section. |
635 | + </tal:comment> |
636 | + <h2>Build status</h2> |
637 | + <p> |
638 | + <span tal:replace="structure context/image:icon" /> |
639 | + <span tal:attributes=" |
640 | + class string:buildstatus${context/status/name};" |
641 | + tal:content="context/status/title"/> |
642 | + <tal:building condition="context/status/enumvalue:BUILDING"> |
643 | + on <a tal:content="context/buildqueue_record/builder/title" |
644 | + tal:attributes="href context/buildqueue_record/builder/fmt:url"/> |
645 | + </tal:building> |
646 | + <tal:built condition="context/builder"> |
647 | + on <a tal:content="context/builder/title" |
648 | + tal:attributes="href context/builder/fmt:url"/> |
649 | + </tal:built> |
650 | + <tal:retry define="link context/menu:context/retry" |
651 | + condition="link/enabled" |
652 | + replace="structure link/fmt:link" /> |
653 | + <tal:cancel define="link context/menu:context/cancel" |
654 | + condition="link/enabled" |
655 | + replace="structure link/fmt:link" /> |
656 | + </p> |
657 | + |
658 | + <ul> |
659 | + <li id="revision-id" tal:condition="context/revision_id"> |
660 | + Revision: <span tal:replace="context/revision_id" /> |
661 | + </li> |
662 | + <li tal:condition="context/dependencies"> |
663 | + Missing build dependencies: <em tal:content="context/dependencies"/> |
664 | + </li> |
665 | + <tal:reallypending condition="context/buildqueue_record"> |
666 | + <tal:pending condition="context/buildqueue_record/status/enumvalue:WAITING"> |
667 | + <li tal:define="eta context/buildqueue_record/getEstimatedJobStartTime"> |
668 | + Start <tal:eta replace="eta/fmt:approximatedate"/> |
669 | + (<span tal:replace="context/buildqueue_record/lastscore"/>) |
670 | + <a href="https://help.launchpad.net/Packaging/BuildScores" |
671 | + target="_blank">What's this?</a> |
672 | + </li> |
673 | + </tal:pending> |
674 | + </tal:reallypending> |
675 | + <tal:started condition="context/date_started"> |
676 | + <li tal:condition="context/date_started"> |
677 | + Started <span |
678 | + tal:define="start context/date_started" |
679 | + tal:attributes="title start/fmt:datetime" |
680 | + tal:content="start/fmt:displaydate"/> |
681 | + </li> |
682 | + </tal:started> |
683 | + <tal:finish condition="not: context/date_finished"> |
684 | + <li tal:define="eta context/eta" tal:condition="context/eta"> |
685 | + Estimated finish <tal:eta replace="eta/fmt:approximatedate"/> |
686 | + </li> |
687 | + </tal:finish> |
688 | + |
689 | + <li tal:condition="context/date_finished"> |
690 | + Finished <span |
691 | + tal:attributes="title context/date_finished/fmt:datetime" |
692 | + tal:content="context/date_finished/fmt:displaydate"/> |
693 | + <tal:duration condition="context/duration"> |
694 | + (took <span tal:replace="context/duration/fmt:exactduration"/>) |
695 | + </tal:duration> |
696 | + </li> |
697 | + <li tal:define="file context/log" |
698 | + tal:condition="file"> |
699 | + <a class="sprite download" |
700 | + tal:attributes="href context/log_url">buildlog</a> |
701 | + (<span tal:replace="file/content/filesize/fmt:bytes" />) |
702 | + </li> |
703 | + <li tal:define="file context/upload_log" |
704 | + tal:condition="file"> |
705 | + <a class="sprite download" |
706 | + tal:attributes="href context/upload_log_url">uploadlog</a> |
707 | + (<span tal:replace="file/content/filesize/fmt:bytes" />) |
708 | + </li> |
709 | + </ul> |
710 | + |
711 | + <div |
712 | + style="margin-top: 1.5em" |
713 | + tal:define="link context/menu:context/rescore" |
714 | + tal:condition="link/enabled" |
715 | + > |
716 | + <a tal:replace="structure link/fmt:link"/> |
717 | + </div> |
718 | + </metal:macro> |
719 | + |
720 | + <metal:macro define-macro="files"> |
721 | + <tal:comment replace="nothing"> |
722 | + Files section. |
723 | + </tal:comment> |
724 | + <h2>Built files</h2> |
725 | + <p>Files resulting from this build:</p> |
726 | + <ul> |
727 | + <li tal:repeat="file view/files"> |
728 | + <a class="sprite download" |
729 | + tal:content="file/filename" |
730 | + tal:attributes="href file/http_url"/> |
731 | + (<span tal:replace="file/content/filesize/fmt:bytes"/>) |
732 | + </li> |
733 | + </ul> |
734 | + </metal:macro> |
735 | + |
736 | + <metal:macro define-macro="buildlog"> |
737 | + <tal:comment replace="nothing"> |
738 | + Buildlog section. |
739 | + </tal:comment> |
740 | + <h2>Buildlog</h2> |
741 | + <div id="buildlog-tail" class="logtail" |
742 | + tal:define="logtail context/buildqueue_record/logtail" |
743 | + tal:content="structure logtail/fmt:text-to-html"/> |
744 | + <p class="lesser" tal:condition="view/user"> |
745 | + Updated on <span tal:replace="structure view/user/fmt:local-time"/> |
746 | + </p> |
747 | + </metal:macro> |
748 | + |
749 | +</metal:macros> |
750 | + |
751 | + </body> |
752 | +</html> |
753 | diff --git a/lib/lp/charms/templates/charmrecipebuild-retry.pt b/lib/lp/charms/templates/charmrecipebuild-retry.pt |
754 | new file mode 100644 |
755 | index 0000000..ba08004 |
756 | --- /dev/null |
757 | +++ b/lib/lp/charms/templates/charmrecipebuild-retry.pt |
758 | @@ -0,0 +1,28 @@ |
759 | +<html |
760 | + xmlns="http://www.w3.org/1999/xhtml" |
761 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
762 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
763 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
764 | + metal:use-macro="view/macro:page/main_only" |
765 | + i18n:domain="launchpad"> |
766 | +<body> |
767 | + |
768 | + <div metal:fill-slot="main"> |
769 | + <div metal:use-macro="context/@@launchpad_form/form"> |
770 | + <div metal:fill-slot="extra_info"> |
771 | + <p> |
772 | + The status of <dfn tal:content="context/title" /> is |
773 | + <span tal:replace="context/status/title" />. |
774 | + </p> |
775 | + <p>Retrying this build will destroy its history and logs.</p> |
776 | + <p> |
777 | + By default, this build will be retried only after other pending |
778 | + builds; please contact a build daemon administrator if you need |
779 | + special treatment. |
780 | + </p> |
781 | + </div> |
782 | + </div> |
783 | + </div> |
784 | + |
785 | +</body> |
786 | +</html> |