launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #26891
[Merge] ~twom/launchpad:oci-add-registry-status-to-recipe-page into launchpad:master
Tom Wardill has proposed merging ~twom/launchpad:oci-add-registry-status-to-recipe-page into launchpad:master.
Commit message:
Add registry upload status to OCIRecipe:+index
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~twom/launchpad/+git/launchpad/+merge/401144
The status of the registry upload for an OCIRecipeBuild was hidden on the build page. The upload could fail, while the build succeeds, leading the +index to confusingly say that everything had succeeded.
Rebuild +index to have a table based on OCIRequestBuildJob, listing the individual builds for a request and their upload status.
Some recipes are old enough to have builds that do not use the async requestBuilds, so munge those into something that looks like a request with a single build purely for display purposes.
This is based on the BuildSetStatus enum, with some later enhancements for the data to be aware of a set of Registry Uploads, as well as Builds.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~twom/launchpad:oci-add-registry-status-to-recipe-page into launchpad:master.
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index 21dad88..f55bf1d 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -43,6 +43,7 @@ from zope.schema import (
ValidationError,
)
from zope.security.interfaces import Unauthorized
+from zope.security.proxy import removeSecurityProxy
from lp.app.browser.launchpadform import (
action,
@@ -75,7 +76,11 @@ from lp.oci.interfaces.ocirecipe import (
OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
OCIRecipeFeatureDisabled,
)
-from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
+from lp.oci.interfaces.ocirecipebuild import (
+ IOCIRecipeBuildSet,
+ OCIRecipeBuildRegistryUploadStatus,
+ )
+from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource
from lp.oci.interfaces.ociregistrycredentials import (
IOCIRegistryCredentialsSet,
OCIRegistryCredentialsAlreadyExist,
@@ -101,6 +106,7 @@ from lp.services.webapp.breadcrumb import NameBreadcrumb
from lp.services.webhooks.browser import WebhookTargetNavigationMixin
from lp.soyuz.browser.archive import EnableProcessorsMixin
from lp.soyuz.browser.build import get_build_by_id_str
+from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
class OCIRecipeNavigation(WebhookTargetNavigationMixin, Navigation):
@@ -308,6 +314,51 @@ class OCIRecipeView(LaunchpadView):
distro = oci_project.distribution
return bool(distro and distro.oci_registry_credentials)
+ def getImageForStatus(self, status):
+ image_map = {
+ BuildSetStatus.NEEDSBUILD: '/@@/build-needed',
+ BuildSetStatus.FULLYBUILT_PENDING: '/@@/build-success-publishing',
+ BuildSetStatus.FAILEDTOBUILD: '/@@/no',
+ BuildSetStatus.BUILDING: '/@@/processing',
+ }
+ return image_map.get(
+ status, '/@@/yes')
+
+ def _convertBuildJobToStatus(self, build_job):
+ recipe_set = getUtility(IOCIRecipeSet)
+ unscheduled_upload = OCIRecipeBuildRegistryUploadStatus.UNSCHEDULED
+ upload_status = build_job.registry_upload_status
+ # This is just a dict, but zope wraps it as RecipeSet is secured
+ status = removeSecurityProxy(
+ recipe_set.getStatusSummaryForBuilds([build_job]))
+ # Add the registry job status
+ status["upload_scheduled"] = upload_status != unscheduled_upload
+ status["upload"] = upload_status
+ status["date"] = build_job.date
+ status["date_estimated"] = build_job.estimate
+ return {
+ "builds": [build_job],
+ "job_id": "build{}".format(build_job.id),
+ "date_created": build_job.date_created,
+ "date_finished": build_job.date_finished,
+ "build_status": status
+ }
+
+ def build_requests(self):
+ req_util = getUtility(IOCIRecipeRequestBuildsJobSource)
+ build_requests = list(req_util.findByOCIRecipe(self.context)[:10])
+
+ # It's possible that some recipes have builds that are older
+ # than the introduction of the async requestBuilds.
+ # In that case, convert the single build to a fake 'request build job'
+ # that has a single attached build.
+ if len(build_requests) < 10:
+ recipe = self.context
+ no_request_builds = recipe.completed_builds_without_build_request
+ for build in no_request_builds[:10 - len(build_requests)]:
+ build_requests.append(self._convertBuildJobToStatus(build))
+ return build_requests[:10]
+
def builds_for_recipe(recipe):
"""A list of interesting builds.
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 7049d9c..3d60ed5 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -1255,25 +1255,53 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
"OCI recipe breadcrumb", "li",
text=re.compile(r"\srecipe-name\s")))))
+ def makeRecipe(self, processor_names, **kwargs):
+ recipe = self.factory.makeOCIRecipe(**kwargs)
+ processors_list = []
+ distroseries = self.factory.makeDistroSeries(
+ distribution=recipe.oci_project.distribution)
+ for proc_name in processor_names:
+ proc = getUtility(IProcessorSet).getByName(proc_name)
+ distro = self.factory.makeDistroArchSeries(
+ distroseries=distroseries, architecturetag=proc_name,
+ processor=proc)
+ distro.addOrUpdateChroot(self.factory.makeLibraryFileAlias())
+ processors_list.append(proc)
+ recipe.setProcessors(processors_list)
+ return recipe
+
def test_index(self):
oci_project = self.factory.makeOCIProject(
pillar=self.distroseries.distribution)
- oci_project_name = oci_project.name
oci_project_display = oci_project.display_name
[ref] = self.factory.makeGitRefs(
owner=self.person, target=self.person, name="recipe-repository",
paths=["refs/heads/master"])
- recipe = self.makeOCIRecipe(
- oci_project=oci_project, git_ref=ref, build_file="Dockerfile")
+ recipe = self.makeRecipe(
+ processor_names=["amd64", "386"],
+ build_file="Dockerfile", git_ref=ref,
+ oci_project=oci_project, registrant=self.person, owner=self.person)
+ build_request = recipe.requestBuilds(self.person)
+ builds = recipe.requestBuildsFromJob(self.person, build_request)
+ job = removeSecurityProxy(build_request).job
+ removeSecurityProxy(job).builds = builds
+
+ for build in builds:
+ removeSecurityProxy(build).updateStatus(
+ BuildStatus.BUILDING, builder=None,
+ date_started=build.date_created)
+ removeSecurityProxy(build).updateStatus(
+ BuildStatus.FULLYBUILT, builder=None,
+ date_finished=build.date_started + timedelta(minutes=30))
+
+ # We also need to account for builds that don't have a build_request
build = self.makeBuild(
recipe=recipe, status=BuildStatus.FULLYBUILT,
duration=timedelta(minutes=30))
- browser = self.getViewBrowser(build.recipe)
+ browser = self.getViewBrowser(build_request.recipe)
login_person(self.person)
self.assertTextMatchesExpressionIgnoreWhitespace("""\
- %s OCI project
- recipe-name
.*
OCI recipe information
Owner: Test Person
@@ -1285,9 +1313,33 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
Official recipe:
No
Latest builds
- Status When complete Architecture
- Successfully built 30 minutes ago 386
- """ % (oci_project_name, oci_project_display, recipe.build_path),
+ Build status
+ Upload status
+ When requested
+ When complete
+ All builds were built successfully.
+ No registry upload requested.
+ a moment ago
+ in 29 minutes
+ amd64
+ Successfully built
+ 386
+ Successfully built
+ amd64
+ in 29 minutes
+ 386
+ in 29 minutes
+ All builds were built successfully.
+ No registry upload requested.
+ 1 hour ago
+ 30 minutes ago
+ 386
+ Successfully built
+ 386
+ 30 minutes ago
+ Recipe push rules
+ This OCI recipe has no push rules defined yet.
+ """ % (oci_project_display, recipe.build_path),
extract_text(find_main_content(browser.contents)))
# Check portlet on side menu.
@@ -1346,8 +1398,18 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
Official recipe:
No
Latest builds
- Status When complete Architecture
- Successfully built 30 minutes ago 386
+ Build status
+ Upload status
+ When requested
+ When complete
+ All builds were built successfully.
+ No registry upload requested.
+ 1 hour ago
+ 30 minutes ago
+ 386
+ Successfully built
+ 386
+ 30 minutes ago
""" % (oci_project_name, oci_project_display, build_path),
self.getMainText(build.recipe))
@@ -1389,8 +1451,18 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
Official recipe:
No
Latest builds
- Status When complete Architecture
- Successfully built 30 minutes ago 386
+ Build status
+ Upload status
+ When requested
+ When complete
+ All builds were built successfully.
+ No registry upload requested.
+ 1 hour ago
+ 30 minutes ago
+ 386
+ Successfully built
+ 386
+ 30 minutes ago
""" % (oci_project_name, oci_project_display, build_path),
main_text)
@@ -1401,8 +1473,20 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
build.setLog(self.factory.makeLibraryFileAlias())
self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
Latest builds
- Status When complete Architecture
- Successfully built 30 minutes ago buildlog \(.*\) 386
+ Build status
+ Upload status
+ When requested
+ When complete
+ All builds were built successfully.
+ No registry upload requested.
+ 1 hour ago
+ 30 minutes ago
+ 386
+ buildlog
+ \(.*\)
+ Successfully built
+ 386
+ 30 minutes ago
""", self.getMainText(build.recipe))
def test_index_no_builds(self):
@@ -1414,13 +1498,30 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
def test_index_pending_build(self):
# A pending build is listed as such.
- build = self.makeBuild()
- build.queueBuild()
+ oci_project = self.factory.makeOCIProject(
+ pillar=self.distroseries.distribution)
+ [ref] = self.factory.makeGitRefs(
+ owner=self.person, target=self.person, name="recipe-repository",
+ paths=["refs/heads/master"])
+ recipe = self.makeRecipe(
+ processor_names=["amd64", "386"],
+ build_file="Dockerfile", git_ref=ref,
+ oci_project=oci_project, registrant=self.person, owner=self.person)
+ build_request = recipe.requestBuilds(self.person)
+ builds = recipe.requestBuildsFromJob(self.person, build_request)
+ job = removeSecurityProxy(build_request).job
+ removeSecurityProxy(job).builds = builds
+ #builds[0].queueBuild()
self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
Latest builds
- Status When complete Architecture
- Needs building in .* \(estimated\) 386
- """, self.getMainText(build.recipe))
+ Build status
+ Upload status
+ When requested
+ When complete
+ There are some builds waiting to be built.
+ a moment ago
+ in .* \(estimated\)
+ """, self.getMainText(recipe))
def test_index_request_builds_link(self):
# Recipe owners get a link to allow requesting builds.
@@ -1531,24 +1632,6 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView):
with dbuser(config.IOCIRecipeRequestBuildsJobSource.dbuser):
JobRunner(jobs).runAll()
- def test_pending_build_requests_not_shown_if_absent(self):
- self.recipe.requestBuilds(self.recipe.owner)
- browser = self.getViewBrowser(self.recipe, user=self.person)
- content = extract_text(find_main_content(browser.contents))
- self.assertIn(
- "You have 1 pending build request. "
- "The builds should start automatically soon.",
- content.replace("\n", " "))
-
- # Run the build request, so we don't have any pending one. The
- # message should be gone then.
- self.runRequestBuildJobs()
- browser = self.getViewBrowser(self.recipe, user=self.person)
- content = extract_text(find_main_content(browser.contents))
- self.assertNotIn(
- "The builds should start automatically soon.",
- content.replace("\n", " "))
-
def test_request_builds_action(self):
# Requesting a build creates pending builds.
browser = self.getViewBrowser(
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index ae682ed..1cd81dd 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -280,6 +280,15 @@ class IOCIRecipeView(Interface):
# Really IOCIRecipeBuild, patched in _schema_circular_imports.
value_type=Reference(schema=Interface), readonly=True)
+ completed_builds_without_build_request = CollectionField(
+ title=_("Completed builds of this OCI recipe."),
+ description=_(
+ "Completed builds of this OCI recipe, sorted in descending "
+ "order of finishing that do no have a corresponding "
+ "build request"),
+ # Really IOCIRecipeBuild, patched in _schema_circular_imports.
+ value_type=Reference(schema=Interface), readonly=True)
+
pending_builds = CollectionField(
title=_("Pending builds of this OCI recipe."),
description=_(
@@ -586,3 +595,25 @@ class IOCIRecipeSet(Interface):
After this, any OCI recipes that previously used this repository
will have no source and so cannot dispatch new builds.
"""
+
+ def getStatusSummaryForBuilds(builds):
+ """Return a summary of the build status for the given builds.
+
+ The returned summary includes a status, a description of
+ that status and the builds related to the status.
+
+ :param builds: A list of build records.
+ :type builds: ``list``
+ :return: A dict consisting of the build status summary for the
+ given builds. For example:
+ {
+ 'status': BuildSetStatus.FULLYBUILT,
+ 'builds': [build1, build2]
+ }
+ or, an example where there are currently some builds building:
+ {
+ 'status': BuildSetStatus.BUILDING,
+ 'builds':[build3]
+ }
+ :rtype: ``dict``.
+ """
diff --git a/lib/lp/oci/interfaces/ocirecipejob.py b/lib/lp/oci/interfaces/ocirecipejob.py
index 2b59faf..5fac142 100644
--- a/lib/lp/oci/interfaces/ocirecipejob.py
+++ b/lib/lp/oci/interfaces/ocirecipejob.py
@@ -90,6 +90,9 @@ class IOCIRecipeRequestBuildsJob(IRunnableJob):
BuildRequest.
"""
+ def build_status():
+ """Return the status of the builds and the upload to a registry."""
+
class IOCIRecipeRequestBuildsJobSource(IJobSource):
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index a56fa7d..3dcb917 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -5,6 +5,9 @@
from __future__ import absolute_import, print_function, unicode_literals
+from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
+
+
__metaclass__ = type
__all__ = [
'get_ocirecipe_privacy_filter',
@@ -732,6 +735,20 @@ class OCIRecipe(Storm, WebhookTargetMixin):
return self._getBuilds(filter_term, order_by)
@property
+ def completed_builds_without_build_request(self):
+ """See `IOCIRecipe`."""
+ filter_term = (
+ Not(OCIRecipeBuild.status.is_in(self._pending_states)),
+ OCIRecipeBuild.build_request_id == None)
+ order_by = (
+ NullsLast(Desc(Greatest(
+ OCIRecipeBuild.date_started,
+ OCIRecipeBuild.date_finished))),
+ Desc(OCIRecipeBuild.id))
+ return self._getBuilds(filter_term, order_by)
+
+
+ @property
def pending_builds(self):
"""See `IOCIRecipe`."""
filter_term = (OCIRecipeBuild.status.is_in(self._pending_states))
@@ -932,6 +949,50 @@ class OCIRecipeSet:
if git_ref is not None:
recipe.git_ref = git_ref
+ def getStatusSummaryForBuilds(self, builds):
+ # Create a small helper function to collect the builds for a given
+ # list of build states:
+ def collect_builds(*states):
+ wanted = []
+ for state in states:
+ candidates = [build for build in builds
+ if build.status == state]
+ wanted.extend(candidates)
+ return wanted
+ failed = collect_builds(BuildStatus.FAILEDTOBUILD,
+ BuildStatus.MANUALDEPWAIT,
+ BuildStatus.CHROOTWAIT,
+ BuildStatus.FAILEDTOUPLOAD)
+ needsbuild = collect_builds(BuildStatus.NEEDSBUILD)
+ building = collect_builds(BuildStatus.BUILDING,
+ BuildStatus.UPLOADING)
+ successful = collect_builds(BuildStatus.FULLYBUILT)
+
+ # Note: the BuildStatus DBItems are used here to summarize the
+ # status of a set of builds:s
+ if len(building) != 0:
+ return {
+ 'status': BuildSetStatus.BUILDING,
+ 'builds': building,
+ }
+ # If there are no builds, this is a 'pending build request'
+ # and needs building
+ elif len(needsbuild) != 0 or len(builds) == 0:
+ return {
+ 'status': BuildSetStatus.NEEDSBUILD,
+ 'builds': needsbuild,
+ }
+ elif len(failed) != 0:
+ return {
+ 'status': BuildSetStatus.FAILEDTOBUILD,
+ 'builds': failed,
+ }
+ else:
+ return {
+ 'status': BuildSetStatus.FULLYBUILT,
+ 'builds': successful,
+ }
+
@implementer(IOCIRecipeBuildRequest)
class OCIRecipeBuildRequest:
diff --git a/lib/lp/oci/model/ocirecipejob.py b/lib/lp/oci/model/ocirecipejob.py
index 3320cff..36e1262 100644
--- a/lib/lp/oci/model/ocirecipejob.py
+++ b/lib/lp/oci/model/ocirecipejob.py
@@ -4,6 +4,9 @@
"""A build job for OCI Recipe."""
from __future__ import absolute_import, print_function, unicode_literals
+from lp.buildmaster.model.processor import Processor
+from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
+
__metaclass__ = type
__all__ = [
@@ -27,8 +30,10 @@ from zope.interface import (
implementer,
provider,
)
+from zope.security.proxy import removeSecurityProxy
from lp.app.errors import NotFoundError
+from lp.oci.interfaces.ocirecipebuild import OCIRecipeBuildRegistryUploadStatus
from lp.oci.interfaces.ocirecipejob import (
IOCIRecipeJob,
IOCIRecipeRequestBuildsJob,
@@ -37,6 +42,8 @@ from lp.oci.interfaces.ocirecipejob import (
from lp.oci.model.ocirecipebuild import OCIRecipeBuild
from lp.registry.interfaces.person import IPersonSet
from lp.services.config import config
+from lp.services.database.bulk import load_related
+from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import EnumCol
from lp.services.database.interfaces import (
IMasterStore,
@@ -51,6 +58,7 @@ from lp.services.job.runner import BaseRunnableJob
from lp.services.mail.sendmail import format_address_for_person
from lp.services.propertycache import cachedproperty
from lp.services.scripts import log
+from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
class OCIRecipeJobType(DBEnumeratedType):
@@ -196,11 +204,17 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
conditions.append(Job._status.is_in(statuses))
if job_ids is not None:
conditions.append(OCIRecipeJob.job_id.is_in(job_ids))
- return IStore(OCIRecipeJob).find(
- (OCIRecipeJob, Job),
+ oci_jobs = IStore(OCIRecipeJob).find(
+ OCIRecipeJob,
OCIRecipeJob.job_id == Job.id,
*conditions).order_by(Desc(OCIRecipeJob.job_id))
+ def preload_jobs(rows):
+ load_related(Job, rows, ["job_id"])
+
+ return DecoratedResultSet(
+ oci_jobs, lambda oci_job: cls(oci_job), pre_iter_hook=preload_jobs)
+
def getOperationDescription(self):
return "requesting builds of %s" % self.recipe
@@ -244,9 +258,13 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
def builds(self):
"""See `OCIRecipeRequestBuildsJob`."""
build_ids = self.metadata.get("builds")
+ # Sort this by architecture/processor name, so it's consistent
+ # when displayed
if build_ids:
return IStore(OCIRecipeBuild).find(
- OCIRecipeBuild, OCIRecipeBuild.id.is_in(build_ids))
+ OCIRecipeBuild, OCIRecipeBuild.id.is_in(build_ids),
+ OCIRecipeBuild.processor_id == Processor.id).order_by(
+ Desc(Processor.name))
else:
return EmptyResultSet()
@@ -270,6 +288,47 @@ class OCIRecipeRequestBuildsJob(OCIRecipeJobDerived):
def addUploadedManifest(self, build_id, manifest_info):
self.metadata["uploaded_manifests"][int(build_id)] = manifest_info
+ def build_status(self):
+ # This just returns a dict, but Zope is really helpful here
+ status = removeSecurityProxy(
+ getUtility(IOCIRecipeSet).getStatusSummaryForBuilds(
+ list(self.builds)))
+
+ # This has a really long name!
+ statusEnum = OCIRecipeBuildRegistryUploadStatus
+
+ # Set the pending upload status if either we're not done uploading,
+ # or there was no upload requested in the first place (no push rules)
+ if status['status'] == BuildSetStatus.FULLYBUILT:
+ upload_status = [
+ (x.registry_upload_status == statusEnum.UPLOADED or
+ x.registry_upload_status == statusEnum.UNSCHEDULED)
+ for x in status['builds']]
+ if not all(upload_status):
+ status['status'] = BuildSetStatus.FULLYBUILT_PENDING
+
+ # Add a flag for if we're expecting a registry upload
+ status['upload_scheduled'] = any(
+ x.registry_upload_status != statusEnum.UNSCHEDULED
+ for x in status['builds'])
+
+ # Set the equivalent of BuildSetStatus, but for registry upload
+ # If any of the builds have failed to upload
+ if any(x.registry_upload_status == statusEnum.FAILEDTOUPLOAD
+ for x in status['builds']):
+ status['upload'] = statusEnum.FAILEDTOUPLOAD
+ # If any of the builds are still waiting to upload
+ elif any(x.registry_upload_status == statusEnum.PENDING
+ for x in status['builds']):
+ status['upload'] = statusEnum.PENDING
+ else:
+ status['upload'] = statusEnum.UPLOADED
+ dates = [x.date for x in self.builds if x.date]
+ status['date'] = max(dates) if dates else None
+ status['date_estimated'] = any(x.estimate for x in self.builds)
+
+ return status
+
def run(self):
"""See `IRunnableJob`."""
requester = self.requester
diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt
index 5a1f3c1..569ea43 100644
--- a/lib/lp/oci/templates/ocirecipe-index.pt
+++ b/lib/lp/oci/templates/ocirecipe-index.pt
@@ -8,6 +8,27 @@
>
<body>
+
+ <div metal:fill-slot="head_epilogue">
+ <script type="text/javascript">
+ LPJS.use('node', 'lp.app.widgets.expander', function(Y) {
+ Y.on('domready', function() {
+ var all_expanders = Y.all('.expander-icon');
+ all_expanders.each(function(icon) {
+ var base_id = icon.get('id').replace('-expander', '');
+ console.log(base_id);
+ var content_node = Y.one('#' + base_id);
+ var animate_node = content_node.one('ul');
+ var expander = new Y.lp.app.widgets.expander.Expander(
+ icon, content_node, { animate_node: animate_node });
+ expander.setUp();
+ });
+ });
+ });
+ </script>
+</div>
+
+
<metal:registering fill-slot="registering">
Created by
<tal:registrant replace="structure context/registrant/fmt:link"/>
@@ -102,55 +123,93 @@
</div>
<h2>Latest builds</h2>
- <div tal:define="count context/pending_build_requests/count|nothing;
- plural string: build requests;
- singular string: build request;"
- tal:condition="count">
- You have <span tal:replace="count" /> pending
- <tal:plural
- metal:use-macro="context/@@+base-layout-macros/plural-message"/>.
- The builds should start automatically soon.
- </div>
<table id="latest-builds-listing" class="listing"
style="margin-bottom: 1em;">
<thead>
<tr>
- <th>Status</th>
+ <th>Build status</th>
+ <th>Upload status</th>
+ <th>When requested</th>
<th>When complete</th>
- <th>Architecture</th>
</tr>
</thead>
<tbody>
- <tal:recipe-builds repeat="item view/builds">
- <tr tal:define="build item"
- tal:attributes="id string:build-${build/id}">
- <td tal:attributes="class string:build_status ${build/status/name}">
- <span tal:replace="structure build/image:icon"/>
- <a tal:content="build/status/title"
- tal:attributes="href build/fmt:url"/>
+ <tal:build-requests repeat="build_request view/build_requests">
+ <tr tal:define="build_status build_request/build_status">
+ <td tal:define="status_img python: view.getImageForStatus(build_status['status'])">
+ <span tal:attributes="id string:request-${build_request/job_id}-expander" class="expander-icon" tal:condition="python: build_status['status'].name is not 'NEEDSBUILD'"> </span>
+ <img tal:attributes="title build_status/status/description;
+ alt build_status/status/description;
+ src status_img" />
+ <span tal:content="build_status/status/description" />
+
</td>
- <td class="datebuilt">
- <tal:date replace="build/date/fmt:displaydate"/>
- <tal:estimate condition="build/estimate">
+ <td>
+ <tal:registry-upload tal:condition="build_status/upload_scheduled">
+ <span tal:content="build_status/upload/title" />
+ </tal:registry-upload>
+ <tal:registry-upload tal:condition="not:build_status/upload_scheduled">
+ <span tal:condition="python: 'FULLYBUILT' in build_status['status'].title">No registry upload requested.</span>
+ <span tal:condition="python: 'FAILEDTOBUILD' in build_status['status'].title">No registry upload requested.</span>
+ </tal:registry-upload>
+ </td>
+ <td>
+ <span tal:content="build_request/date_created/fmt:displaydate" />
+ </td>
+ <td>
+ <span tal:content="build_status/date/fmt:displaydate" />
+ <tal:estimate condition="build_status/date_estimated">
(estimated)
</tal:estimate>
-
- <tal:build-log define="file build/log" tal:condition="file">
- <a class="sprite download"
- tal:attributes="href build/log_url">buildlog</a>
- (<span tal:replace="file/content/filesize/fmt:bytes"/>)
- </tal:build-log>
</td>
+ </tr>
+ <tr tal:define="build_status build_request/build_status" tal:attributes="id string:request-${build_request/job_id}" tal:condition="python: build_status['status'].name is not 'NEEDSBUILD'">
<td>
- <!-- XXX cjwatson 2020-02-19: This should show a DAS
- architecture tag rather than a processor name once we can
- do that. -->
- <a class="sprite distribution"
- tal:define="processor build/processor"
- tal:content="processor/name"/>
+ <ul tal:repeat="build build_request/builds">
+ <li style="padding-left: 22px;">
+ <strong>
+ <a class="sprite distribution"
+ tal:define="processor build/processor"
+ tal:content="processor/name"
+ tal:attributes="href build/fmt:url"/>
+ </strong>
+ <span tal:define="file build/log" tal:condition="file">
+ <a class="sprite download"
+ tal:attributes="href build/log_url">buildlog</a>
+ (<span tal:replace="file/content/filesize/fmt:bytes"/>)
+ </span>
+ <span tal:content="build/status/title" />
+ </li>
+ </ul>
</td>
+ <td>
+ <ul tal:condition="build_status/upload_scheduled" tal:repeat="build build_request/builds">
+ <li style="padding-left: 22px;">
+ <strong><a class="sprite distribution"
+ tal:define="processor build/processor"
+ tal:content="processor/name"/></strong>
+ <span tal:content="build/registry_upload_status/title" />
+ </li>
+ </ul>
+ </td>
+ <td>
+ </td>
+ <td>
+ <ul tal:repeat="build build_request/builds">
+ <li style="padding-left: 22px;">
+ <strong><a class="sprite distribution"
+ tal:define="processor build/processor"
+ tal:content="processor/name"/></strong>
+ <span tal:content="build/date/fmt:displaydate" />
+ <tal:estimate condition="build/estimate">
+ (estimated)
+ </tal:estimate>
+ </li>
+ </ul>
+ </td>
+
</tr>
- </tal:recipe-builds>
+ </tal:build-requests>
</tbody>
</table>
<p tal:condition="not: view/builds">
Follow ups