Merge ~cjwatson/launchpad:charm-recipe-build-basic-browser into launchpad: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)
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

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
1diff --git a/lib/lp/app/browser/configure.zcml b/lib/lp/app/browser/configure.zcml
2index 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"
18diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
19index 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
41diff --git a/lib/lp/charms/browser/charmrecipebuild.py b/lib/lp/charms/browser/charmrecipebuild.py
42new file mode 100644
43index 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)}
218diff --git a/lib/lp/charms/browser/configure.zcml b/lib/lp/charms/browser/configure.zcml
219index 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>
276diff --git a/lib/lp/charms/browser/tests/test_charmrecipebuild.py b/lib/lp/charms/browser/tests/test_charmrecipebuild.py
277new file mode 100644
278index 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)
546diff --git a/lib/lp/charms/templates/charmrecipebuild-index.pt b/lib/lp/charms/templates/charmrecipebuild-index.pt
547new file mode 100644
548index 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>
753diff --git a/lib/lp/charms/templates/charmrecipebuild-retry.pt b/lib/lp/charms/templates/charmrecipebuild-retry.pt
754new file mode 100644
755index 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>

Subscribers

People subscribed via source and target branches

to status/vote changes: