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
diff --git a/lib/lp/app/browser/configure.zcml b/lib/lp/app/browser/configure.zcml
index b08d380..005968c 100644
--- a/lib/lp/app/browser/configure.zcml
+++ b/lib/lp/app/browser/configure.zcml
@@ -877,6 +877,12 @@
877 name="fmt"877 name="fmt"
878 />878 />
879 <adapter879 <adapter
880 for="lp.charms.interfaces.charmrecipe.ICharmRecipe"
881 provides="zope.traversing.interfaces.IPathAdapter"
882 factory="lp.app.browser.tales.CharmRecipeFormatterAPI"
883 name="fmt"
884 />
885 <adapter
880 for="lp.blueprints.interfaces.specification.ISpecification"886 for="lp.blueprints.interfaces.specification.ISpecification"
881 provides="zope.traversing.interfaces.IPathAdapter"887 provides="zope.traversing.interfaces.IPathAdapter"
882 factory="lp.app.browser.tales.SpecificationFormatterAPI"888 factory="lp.app.browser.tales.SpecificationFormatterAPI"
diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
index 17fd33f..1bf84aa 100644
--- a/lib/lp/app/browser/tales.py
+++ b/lib/lp/app/browser/tales.py
@@ -1951,6 +1951,18 @@ class SnappySeriesFormatterAPI(CustomizableFormatter):
1951 return {'title': self._context.title}1951 return {'title': self._context.title}
19521952
19531953
1954class CharmRecipeFormatterAPI(CustomizableFormatter):
1955 """Adapter providing fmt support for ICharmRecipe objects."""
1956
1957 _link_summary_template = _(
1958 'Charm recipe %(name)s for %(owner)s in %(project)s')
1959
1960 def _link_summary_values(self):
1961 return {'name': self._context.name,
1962 'owner': self._context.owner.displayname,
1963 'project': self._context.project.displayname}
1964
1965
1954class SpecificationFormatterAPI(CustomizableFormatter):1966class SpecificationFormatterAPI(CustomizableFormatter):
1955 """Adapter providing fmt support for Specification objects"""1967 """Adapter providing fmt support for Specification objects"""
19561968
diff --git a/lib/lp/charms/browser/charmrecipebuild.py b/lib/lp/charms/browser/charmrecipebuild.py
1957new file mode 1006441969new file mode 100644
index 0000000..f695084
--- /dev/null
+++ b/lib/lp/charms/browser/charmrecipebuild.py
@@ -0,0 +1,171 @@
1# Copyright 2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Charm recipe build views."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 "CharmRecipeBuildContextMenu",
11 "CharmRecipeBuildNavigation",
12 "CharmRecipeBuildView",
13 ]
14
15from zope.interface import Interface
16
17from lp.app.browser.launchpadform import (
18 action,
19 LaunchpadFormView,
20 )
21from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
22from lp.services.librarian.browser import (
23 FileNavigationMixin,
24 ProxiedLibraryFileAlias,
25 )
26from lp.services.propertycache import cachedproperty
27from lp.services.webapp import (
28 canonical_url,
29 ContextMenu,
30 enabled_with_permission,
31 LaunchpadView,
32 Link,
33 Navigation,
34 )
35from lp.soyuz.interfaces.binarypackagebuild import IBuildRescoreForm
36
37
38class CharmRecipeBuildNavigation(Navigation, FileNavigationMixin):
39 usedfor = ICharmRecipeBuild
40
41
42class CharmRecipeBuildContextMenu(ContextMenu):
43 """Context menu for charm recipe builds."""
44
45 usedfor = ICharmRecipeBuild
46
47 facet = "overview"
48
49 links = ("retry", "cancel", "rescore")
50
51 @enabled_with_permission("launchpad.Edit")
52 def retry(self):
53 return Link(
54 "+retry", "Retry this build", icon="retry",
55 enabled=self.context.can_be_retried)
56
57 @enabled_with_permission("launchpad.Edit")
58 def cancel(self):
59 return Link(
60 "+cancel", "Cancel build", icon="remove",
61 enabled=self.context.can_be_cancelled)
62
63 @enabled_with_permission("launchpad.Admin")
64 def rescore(self):
65 return Link(
66 "+rescore", "Rescore build", icon="edit",
67 enabled=self.context.can_be_rescored)
68
69
70class CharmRecipeBuildView(LaunchpadView):
71 """Default view of a charm recipe build."""
72
73 @property
74 def label(self):
75 return self.context.title
76
77 page_title = label
78
79 @cachedproperty
80 def files(self):
81 """Return `LibraryFileAlias`es for files produced by this build."""
82 if not self.context.was_built:
83 return None
84
85 return [
86 ProxiedLibraryFileAlias(alias, self.context)
87 for _, alias, _ in self.context.getFiles() if not alias.deleted]
88
89 @cachedproperty
90 def has_files(self):
91 return bool(self.files)
92
93 @property
94 def next_url(self):
95 return canonical_url(self.context)
96
97
98class CharmRecipeBuildRetryView(LaunchpadFormView):
99 """View for retrying a charm recipe build."""
100
101 class schema(Interface):
102 """Schema for retrying a build."""
103
104 page_title = label = "Retry build"
105
106 @property
107 def cancel_url(self):
108 return canonical_url(self.context)
109 next_url = cancel_url
110
111 @action("Retry build", name="retry")
112 def request_action(self, action, data):
113 """Retry the build."""
114 if not self.context.can_be_retried:
115 self.request.response.addErrorNotification(
116 "Build cannot be retried")
117 else:
118 self.context.retry()
119 self.request.response.addInfoNotification("Build has been queued")
120
121 self.request.response.redirect(self.next_url)
122
123
124class CharmRecipeBuildCancelView(LaunchpadFormView):
125 """View for cancelling a charm recipe build."""
126
127 class schema(Interface):
128 """Schema for cancelling a build."""
129
130 page_title = label = "Cancel build"
131
132 @property
133 def cancel_url(self):
134 return canonical_url(self.context)
135 next_url = cancel_url
136
137 @action("Cancel build", name="cancel")
138 def request_action(self, action, data):
139 """Cancel the build."""
140 self.context.cancel()
141
142
143class CharmRecipeBuildRescoreView(LaunchpadFormView):
144 """View for rescoring a charm recipe build."""
145
146 schema = IBuildRescoreForm
147
148 page_title = label = "Rescore build"
149
150 def __call__(self):
151 if self.context.can_be_rescored:
152 return super(CharmRecipeBuildRescoreView, self).__call__()
153 self.request.response.addWarningNotification(
154 "Cannot rescore this build because it is not queued.")
155 self.request.response.redirect(canonical_url(self.context))
156
157 @property
158 def cancel_url(self):
159 return canonical_url(self.context)
160 next_url = cancel_url
161
162 @action("Rescore build", name="rescore")
163 def request_action(self, action, data):
164 """Rescore the build."""
165 score = data.get("priority")
166 self.context.rescore(score)
167 self.request.response.addNotification("Build rescored to %s." % score)
168
169 @property
170 def initial_values(self):
171 return {"score": str(self.context.buildqueue_record.lastscore)}
diff --git a/lib/lp/charms/browser/configure.zcml b/lib/lp/charms/browser/configure.zcml
index 92d78bf..0475288 100644
--- a/lib/lp/charms/browser/configure.zcml
+++ b/lib/lp/charms/browser/configure.zcml
@@ -28,13 +28,53 @@
28 for="lp.charms.interfaces.charmrecipe.ICharmRecipe"28 for="lp.charms.interfaces.charmrecipe.ICharmRecipe"
29 factory="lp.charms.browser.charmrecipe.CharmRecipeBreadcrumb"29 factory="lp.charms.browser.charmrecipe.CharmRecipeBreadcrumb"
30 permission="zope.Public" />30 permission="zope.Public" />
31
31 <browser:url32 <browser:url
32 for="lp.charms.interfaces.charmrecipe.ICharmRecipeBuildRequest"33 for="lp.charms.interfaces.charmrecipe.ICharmRecipeBuildRequest"
33 path_expression="string:+build-request/${id}"34 path_expression="string:+build-request/${id}"
34 attribute_to_parent="recipe" />35 attribute_to_parent="recipe" />
36
35 <browser:url37 <browser:url
36 for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild"38 for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild"
37 path_expression="string:+build/${id}"39 path_expression="string:+build/${id}"
38 attribute_to_parent="recipe" />40 attribute_to_parent="recipe" />
41 <browser:menus
42 module="lp.charms.browser.charmrecipebuild"
43 classes="CharmRecipeBuildContextMenu" />
44 <browser:navigation
45 module="lp.charms.browser.charmrecipebuild"
46 classes="CharmRecipeBuildNavigation" />
47 <browser:defaultView
48 for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild"
49 name="+index" />
50 <browser:page
51 for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild"
52 class="lp.charms.browser.charmrecipebuild.CharmRecipeBuildView"
53 permission="launchpad.View"
54 name="+index"
55 template="../templates/charmrecipebuild-index.pt" />
56 <browser:page
57 for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild"
58 class="lp.charms.browser.charmrecipebuild.CharmRecipeBuildRetryView"
59 permission="launchpad.Edit"
60 name="+retry"
61 template="../templates/charmrecipebuild-retry.pt" />
62 <browser:page
63 for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild"
64 class="lp.charms.browser.charmrecipebuild.CharmRecipeBuildCancelView"
65 permission="launchpad.Edit"
66 name="+cancel"
67 template="../../app/templates/generic-edit.pt" />
68 <browser:page
69 for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild"
70 class="lp.charms.browser.charmrecipebuild.CharmRecipeBuildRescoreView"
71 permission="launchpad.Admin"
72 name="+rescore"
73 template="../../app/templates/generic-edit.pt" />
74 <adapter
75 provides="lp.services.webapp.interfaces.IBreadcrumb"
76 for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild"
77 factory="lp.services.webapp.breadcrumb.TitleBreadcrumb"
78 permission="zope.Public" />
39 </facet>79 </facet>
40</configure>80</configure>
diff --git a/lib/lp/charms/browser/tests/test_charmrecipebuild.py b/lib/lp/charms/browser/tests/test_charmrecipebuild.py
41new file mode 10064481new file mode 100644
index 0000000..0978a0a
--- /dev/null
+++ b/lib/lp/charms/browser/tests/test_charmrecipebuild.py
@@ -0,0 +1,264 @@
1# Copyright 2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test charm recipe build views."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9
10import re
11
12from fixtures import FakeLogger
13import soupmatchers
14from storm.locals import Store
15from testtools.matchers import StartsWith
16import transaction
17from zope.component import getUtility
18from zope.security.interfaces import Unauthorized
19from zope.security.proxy import removeSecurityProxy
20from zope.testbrowser.browser import LinkNotFoundError
21
22from lp.app.enums import InformationType
23from lp.app.interfaces.launchpad import ILaunchpadCelebrities
24from lp.buildmaster.enums import BuildStatus
25from lp.charms.interfaces.charmrecipe import (
26 CHARM_RECIPE_ALLOW_CREATE,
27 CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
28 )
29from lp.services.features.testing import FeatureFixture
30from lp.services.webapp import canonical_url
31from lp.testing import (
32 ANONYMOUS,
33 BrowserTestCase,
34 login,
35 person_logged_in,
36 TestCaseWithFactory,
37 )
38from lp.testing.layers import (
39 DatabaseFunctionalLayer,
40 LaunchpadFunctionalLayer,
41 )
42from lp.testing.pages import (
43 extract_text,
44 find_main_content,
45 find_tags_by_class,
46 )
47from lp.testing.views import create_initialized_view
48
49
50class TestCanonicalUrlForCharmRecipeBuild(TestCaseWithFactory):
51
52 layer = DatabaseFunctionalLayer
53
54 def setUp(self):
55 super(TestCanonicalUrlForCharmRecipeBuild, self).setUp()
56 self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
57
58 def test_canonical_url(self):
59 owner = self.factory.makePerson(name="person")
60 project = self.factory.makeProduct(name="charm-project")
61 recipe = self.factory.makeCharmRecipe(
62 registrant=owner, owner=owner, project=project, name="charm")
63 build = self.factory.makeCharmRecipeBuild(
64 requester=owner, recipe=recipe)
65 self.assertThat(
66 canonical_url(build),
67 StartsWith(
68 "http://launchpad.test/~person/charm-project/+charm/charm/"
69 "+build/"))
70
71
72class TestCharmRecipeBuildView(TestCaseWithFactory):
73
74 layer = LaunchpadFunctionalLayer
75
76 def setUp(self):
77 super(TestCharmRecipeBuildView, self).setUp()
78 self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
79
80 def test_files(self):
81 # CharmRecipeBuildView.files returns all the associated files.
82 build = self.factory.makeCharmRecipeBuild(
83 status=BuildStatus.FULLYBUILT)
84 charm_file = self.factory.makeCharmFile(build=build)
85 build_view = create_initialized_view(build, "+index")
86 self.assertEqual(
87 [charm_file.library_file.filename],
88 [lfa.filename for lfa in build_view.files])
89 # Deleted files won't be included.
90 self.assertFalse(charm_file.library_file.deleted)
91 removeSecurityProxy(charm_file.library_file).content = None
92 self.assertTrue(charm_file.library_file.deleted)
93 build_view = create_initialized_view(build, "+index")
94 self.assertEqual([], build_view.files)
95
96 def test_revision_id(self):
97 build = self.factory.makeCharmRecipeBuild()
98 build.updateStatus(
99 BuildStatus.FULLYBUILT, slave_status={"revision_id": "dummy"})
100 build_view = create_initialized_view(build, "+index")
101 self.assertThat(build_view(), soupmatchers.HTMLContains(
102 soupmatchers.Tag(
103 "revision ID", "li", attrs={"id": "revision-id"},
104 text=re.compile(r"^\s*Revision: dummy\s*$"))))
105
106
107class TestCharmRecipeBuildOperations(BrowserTestCase):
108
109 layer = DatabaseFunctionalLayer
110
111 def setUp(self):
112 super(TestCharmRecipeBuildOperations, self).setUp()
113 self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
114 self.useFixture(FakeLogger())
115 self.build = self.factory.makeCharmRecipeBuild()
116 self.build_url = canonical_url(self.build)
117 self.requester = self.build.requester
118 self.buildd_admin = self.factory.makePerson(
119 member_of=[getUtility(ILaunchpadCelebrities).buildd_admin])
120
121 def test_retry_build(self):
122 # The requester of a build can retry it.
123 self.build.updateStatus(BuildStatus.FAILEDTOBUILD)
124 transaction.commit()
125 browser = self.getViewBrowser(self.build, user=self.requester)
126 browser.getLink("Retry this build").click()
127 self.assertEqual(self.build_url, browser.getLink("Cancel").url)
128 browser.getControl("Retry build").click()
129 self.assertEqual(self.build_url, browser.url)
130 login(ANONYMOUS)
131 self.assertEqual(BuildStatus.NEEDSBUILD, self.build.status)
132
133 def test_retry_build_random_user(self):
134 # An unrelated non-admin user cannot retry a build.
135 self.build.updateStatus(BuildStatus.FAILEDTOBUILD)
136 transaction.commit()
137 user = self.factory.makePerson()
138 browser = self.getViewBrowser(self.build, user=user)
139 self.assertRaises(
140 LinkNotFoundError, browser.getLink, "Retry this build")
141 self.assertRaises(
142 Unauthorized, self.getUserBrowser, self.build_url + "/+retry",
143 user=user)
144
145 def test_retry_build_wrong_state(self):
146 # If the build isn't in an unsuccessful terminal state, you can't
147 # retry it.
148 self.build.updateStatus(BuildStatus.FULLYBUILT)
149 browser = self.getViewBrowser(self.build, user=self.requester)
150 self.assertRaises(
151 LinkNotFoundError, browser.getLink, "Retry this build")
152
153 def test_cancel_build(self):
154 # The requester of a build can cancel it.
155 self.build.queueBuild()
156 transaction.commit()
157 browser = self.getViewBrowser(self.build, user=self.requester)
158 browser.getLink("Cancel build").click()
159 self.assertEqual(self.build_url, browser.getLink("Cancel").url)
160 browser.getControl("Cancel build").click()
161 self.assertEqual(self.build_url, browser.url)
162 login(ANONYMOUS)
163 self.assertEqual(BuildStatus.CANCELLED, self.build.status)
164
165 def test_cancel_build_random_user(self):
166 # An unrelated non-admin user cannot cancel a build.
167 self.build.queueBuild()
168 transaction.commit()
169 user = self.factory.makePerson()
170 browser = self.getViewBrowser(self.build, user=user)
171 self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build")
172 self.assertRaises(
173 Unauthorized, self.getUserBrowser, self.build_url + "/+cancel",
174 user=user)
175
176 def test_cancel_build_wrong_state(self):
177 # If the build isn't queued, you can't cancel it.
178 browser = self.getViewBrowser(self.build, user=self.requester)
179 self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build")
180
181 def test_rescore_build(self):
182 # A buildd admin can rescore a build.
183 self.build.queueBuild()
184 transaction.commit()
185 browser = self.getViewBrowser(self.build, user=self.buildd_admin)
186 browser.getLink("Rescore build").click()
187 self.assertEqual(self.build_url, browser.getLink("Cancel").url)
188 browser.getControl("Priority").value = "1024"
189 browser.getControl("Rescore build").click()
190 self.assertEqual(self.build_url, browser.url)
191 login(ANONYMOUS)
192 self.assertEqual(1024, self.build.buildqueue_record.lastscore)
193
194 def test_rescore_build_invalid_score(self):
195 # Build scores can only take numbers.
196 self.build.queueBuild()
197 transaction.commit()
198 browser = self.getViewBrowser(self.build, user=self.buildd_admin)
199 browser.getLink("Rescore build").click()
200 self.assertEqual(self.build_url, browser.getLink("Cancel").url)
201 browser.getControl("Priority").value = "tentwentyfour"
202 browser.getControl("Rescore build").click()
203 self.assertEqual(
204 "Invalid integer data",
205 extract_text(find_tags_by_class(browser.contents, "message")[1]))
206
207 def test_rescore_build_not_admin(self):
208 # A non-admin user cannot cancel a build.
209 self.build.queueBuild()
210 transaction.commit()
211 user = self.factory.makePerson()
212 browser = self.getViewBrowser(self.build, user=user)
213 self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build")
214 self.assertRaises(
215 Unauthorized, self.getUserBrowser, self.build_url + "/+rescore",
216 user=user)
217
218 def test_rescore_build_wrong_state(self):
219 # If the build isn't NEEDSBUILD, you can't rescore it.
220 self.build.queueBuild()
221 with person_logged_in(self.requester):
222 self.build.cancel()
223 browser = self.getViewBrowser(self.build, user=self.buildd_admin)
224 self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build")
225
226 def test_rescore_build_wrong_state_stale_link(self):
227 # An attempt to rescore a non-queued build from a stale link shows a
228 # sensible error message.
229 self.build.queueBuild()
230 with person_logged_in(self.requester):
231 self.build.cancel()
232 browser = self.getViewBrowser(
233 self.build, "+rescore", user=self.buildd_admin)
234 self.assertEqual(self.build_url, browser.url)
235 self.assertThat(browser.contents, soupmatchers.HTMLContains(
236 soupmatchers.Tag(
237 "notification", "div", attrs={"class": "warning message"},
238 text="Cannot rescore this build because it is not queued.")))
239
240 def test_builder_history(self):
241 Store.of(self.build).flush()
242 self.build.updateStatus(
243 BuildStatus.FULLYBUILT, builder=self.factory.makeBuilder())
244 title = self.build.title
245 browser = self.getViewBrowser(self.build.builder, "+history")
246 self.assertTextMatchesExpressionIgnoreWhitespace(
247 r"Build history.*%s" % re.escape(title),
248 extract_text(find_main_content(browser.contents)))
249 self.assertEqual(self.build_url, browser.getLink(title).url)
250
251 def makeBuildingRecipe(self, information_type=InformationType.PUBLIC):
252 builder = self.factory.makeBuilder()
253 build = self.factory.makeCharmRecipeBuild(
254 information_type=information_type)
255 build.updateStatus(BuildStatus.BUILDING, builder=builder)
256 build.queueBuild()
257 build.buildqueue_record.builder = builder
258 build.buildqueue_record.logtail = "tail of the log"
259 return build
260
261 def test_builder_index_public(self):
262 build = self.makeBuildingRecipe()
263 browser = self.getViewBrowser(build.builder, no_login=True)
264 self.assertIn("tail of the log", browser.contents)
diff --git a/lib/lp/charms/templates/charmrecipebuild-index.pt b/lib/lp/charms/templates/charmrecipebuild-index.pt
0new file mode 100644265new file mode 100644
index 0000000..1d0e4f0
--- /dev/null
+++ b/lib/lp/charms/templates/charmrecipebuild-index.pt
@@ -0,0 +1,201 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9
10 <body>
11
12 <tal:registering metal:fill-slot="registering">
13 created
14 <span tal:content="context/date_created/fmt:displaydate"
15 tal:attributes="title context/date_created/fmt:datetime"/>
16 </tal:registering>
17
18 <div metal:fill-slot="main">
19
20 <div class="yui-g">
21
22 <div id="status" class="yui-u first">
23 <div class="portlet">
24 <div metal:use-macro="template/macros/status"/>
25 </div>
26 </div>
27
28 <div id="details" class="yui-u">
29 <div class="portlet">
30 <div metal:use-macro="template/macros/details"/>
31 </div>
32 </div>
33
34 </div> <!-- yui-g -->
35
36 <div id="files" class="portlet" tal:condition="view/has_files">
37 <div metal:use-macro="template/macros/files"/>
38 </div>
39
40 <div id="buildlog" class="portlet"
41 tal:condition="context/status/enumvalue:BUILDING">
42 <div metal:use-macro="template/macros/buildlog"/>
43 </div>
44
45 </div> <!-- main -->
46
47
48<metal:macros fill-slot="bogus">
49
50 <metal:macro define-macro="details">
51 <tal:comment replace="nothing">
52 Details section.
53 </tal:comment>
54 <h2>Build details</h2>
55 <div class="two-column-list">
56 <dl>
57 <dt>Recipe:</dt>
58 <dd>
59 <tal:recipe replace="structure context/recipe/fmt:link"/>
60 </dd>
61 </dl>
62 <dl>
63 <dt>Series:</dt>
64 <dd><a class="sprite distribution"
65 tal:define="series context/distro_series"
66 tal:attributes="href series/fmt:url"
67 tal:content="series/displayname"/>
68 </dd>
69 </dl>
70 <dl>
71 <dt>Architecture:</dt>
72 <dd><a class="sprite distribution"
73 tal:define="archseries context/distro_arch_series"
74 tal:attributes="href archseries/fmt:url"
75 tal:content="archseries/architecturetag"/>
76 </dd>
77 </dl>
78 </div>
79 </metal:macro>
80
81 <metal:macro define-macro="status">
82 <tal:comment replace="nothing">
83 Status section.
84 </tal:comment>
85 <h2>Build status</h2>
86 <p>
87 <span tal:replace="structure context/image:icon" />
88 <span tal:attributes="
89 class string:buildstatus${context/status/name};"
90 tal:content="context/status/title"/>
91 <tal:building condition="context/status/enumvalue:BUILDING">
92 on <a tal:content="context/buildqueue_record/builder/title"
93 tal:attributes="href context/buildqueue_record/builder/fmt:url"/>
94 </tal:building>
95 <tal:built condition="context/builder">
96 on <a tal:content="context/builder/title"
97 tal:attributes="href context/builder/fmt:url"/>
98 </tal:built>
99 <tal:retry define="link context/menu:context/retry"
100 condition="link/enabled"
101 replace="structure link/fmt:link" />
102 <tal:cancel define="link context/menu:context/cancel"
103 condition="link/enabled"
104 replace="structure link/fmt:link" />
105 </p>
106
107 <ul>
108 <li id="revision-id" tal:condition="context/revision_id">
109 Revision: <span tal:replace="context/revision_id" />
110 </li>
111 <li tal:condition="context/dependencies">
112 Missing build dependencies: <em tal:content="context/dependencies"/>
113 </li>
114 <tal:reallypending condition="context/buildqueue_record">
115 <tal:pending condition="context/buildqueue_record/status/enumvalue:WAITING">
116 <li tal:define="eta context/buildqueue_record/getEstimatedJobStartTime">
117 Start <tal:eta replace="eta/fmt:approximatedate"/>
118 (<span tal:replace="context/buildqueue_record/lastscore"/>)
119 <a href="https://help.launchpad.net/Packaging/BuildScores"
120 target="_blank">What's this?</a>
121 </li>
122 </tal:pending>
123 </tal:reallypending>
124 <tal:started condition="context/date_started">
125 <li tal:condition="context/date_started">
126 Started <span
127 tal:define="start context/date_started"
128 tal:attributes="title start/fmt:datetime"
129 tal:content="start/fmt:displaydate"/>
130 </li>
131 </tal:started>
132 <tal:finish condition="not: context/date_finished">
133 <li tal:define="eta context/eta" tal:condition="context/eta">
134 Estimated finish <tal:eta replace="eta/fmt:approximatedate"/>
135 </li>
136 </tal:finish>
137
138 <li tal:condition="context/date_finished">
139 Finished <span
140 tal:attributes="title context/date_finished/fmt:datetime"
141 tal:content="context/date_finished/fmt:displaydate"/>
142 <tal:duration condition="context/duration">
143 (took <span tal:replace="context/duration/fmt:exactduration"/>)
144 </tal:duration>
145 </li>
146 <li tal:define="file context/log"
147 tal:condition="file">
148 <a class="sprite download"
149 tal:attributes="href context/log_url">buildlog</a>
150 (<span tal:replace="file/content/filesize/fmt:bytes" />)
151 </li>
152 <li tal:define="file context/upload_log"
153 tal:condition="file">
154 <a class="sprite download"
155 tal:attributes="href context/upload_log_url">uploadlog</a>
156 (<span tal:replace="file/content/filesize/fmt:bytes" />)
157 </li>
158 </ul>
159
160 <div
161 style="margin-top: 1.5em"
162 tal:define="link context/menu:context/rescore"
163 tal:condition="link/enabled"
164 >
165 <a tal:replace="structure link/fmt:link"/>
166 </div>
167 </metal:macro>
168
169 <metal:macro define-macro="files">
170 <tal:comment replace="nothing">
171 Files section.
172 </tal:comment>
173 <h2>Built files</h2>
174 <p>Files resulting from this build:</p>
175 <ul>
176 <li tal:repeat="file view/files">
177 <a class="sprite download"
178 tal:content="file/filename"
179 tal:attributes="href file/http_url"/>
180 (<span tal:replace="file/content/filesize/fmt:bytes"/>)
181 </li>
182 </ul>
183 </metal:macro>
184
185 <metal:macro define-macro="buildlog">
186 <tal:comment replace="nothing">
187 Buildlog section.
188 </tal:comment>
189 <h2>Buildlog</h2>
190 <div id="buildlog-tail" class="logtail"
191 tal:define="logtail context/buildqueue_record/logtail"
192 tal:content="structure logtail/fmt:text-to-html"/>
193 <p class="lesser" tal:condition="view/user">
194 Updated on <span tal:replace="structure view/user/fmt:local-time"/>
195 </p>
196 </metal:macro>
197
198</metal:macros>
199
200 </body>
201</html>
diff --git a/lib/lp/charms/templates/charmrecipebuild-retry.pt b/lib/lp/charms/templates/charmrecipebuild-retry.pt
0new file mode 100644202new file mode 100644
index 0000000..ba08004
--- /dev/null
+++ b/lib/lp/charms/templates/charmrecipebuild-retry.pt
@@ -0,0 +1,28 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad">
8<body>
9
10 <div metal:fill-slot="main">
11 <div metal:use-macro="context/@@launchpad_form/form">
12 <div metal:fill-slot="extra_info">
13 <p>
14 The status of <dfn tal:content="context/title" /> is
15 <span tal:replace="context/status/title" />.
16 </p>
17 <p>Retrying this build will destroy its history and logs.</p>
18 <p>
19 By default, this build will be retried only after other pending
20 builds; please contact a build daemon administrator if you need
21 special treatment.
22 </p>
23 </div>
24 </div>
25 </div>
26
27</body>
28</html>

Subscribers

People subscribed via source and target branches

to status/vote changes: