launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22937
[Merge] lp:~cjwatson/launchpad/snap-failed-build-requests into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-failed-build-requests into lp:launchpad.
Commit message:
Show recent failed build requests on Snap:+index.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1770400 in Launchpad itself: "Support snapcraft architectures keyword"
https://bugs.launchpad.net/launchpad/+bug/1770400
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-failed-build-requests/+merge/355773
This is a bit fiddly since we need to interleave various items for the "Latest builds" table, but fortunately the total number of non-pending items we display there is bounded so we don't need to be too clever.
The new export of Snap.failed_build_requests is needed to implement the design proposals in https://github.com/canonical-websites/build.snapcraft.io/issues/556.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-failed-build-requests into lp:launchpad.
=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2018-06-21 17:26:43 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2018-09-27 13:59:29 +0000
@@ -93,6 +93,7 @@
from lp.services.librarian.interfaces.client import LibrarianServerError
from lp.services.messages.model.message import MessageSet
from lp.services.timeout import TimeoutError
+from lp.services.utils import seconds_since_epoch
from lp.services.webapp import canonical_url
from lp.services.webapp.interfaces import BrowserNotificationLevel
from lp.services.webapp.servers import LaunchpadTestRequest
@@ -1515,7 +1516,6 @@
author = self.factory.makePerson()
with person_logged_in(author):
author_email = author.preferredemail.email
- epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
review_date = self.factory.getUniqueDate()
commit_date = self.factory.getUniqueDate()
bmp = self.factory.makeBranchMergeProposalForGit(
@@ -1527,7 +1527,7 @@
'author': {
'name': author.display_name,
'email': author_email,
- 'time': int((commit_date - epoch).total_seconds()),
+ 'time': int(seconds_since_epoch(commit_date)),
},
}
]))
@@ -1614,7 +1614,6 @@
# SHA-1 and can ask the repository for its unmerged commits.
bmp = self.factory.makeBranchMergeProposalForGit()
sha1 = unicode(hashlib.sha1(b'0').hexdigest())
- epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
commit_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
self.useFixture(GitHostingFixture(log=[
{
@@ -1623,7 +1622,7 @@
'author': {
'name': 'Example Person',
'email': 'person@xxxxxxxxxxx',
- 'time': int((commit_date - epoch).total_seconds()),
+ 'time': int(seconds_since_epoch(commit_date)),
},
}
]))
=== modified file 'lib/lp/code/browser/tests/test_gitref.py'
--- lib/lp/code/browser/tests/test_gitref.py 2018-09-07 13:43:50 +0000
+++ lib/lp/code/browser/tests/test_gitref.py 2018-09-27 13:59:29 +0000
@@ -23,6 +23,7 @@
from lp.code.tests.helpers import GitHostingFixture
from lp.services.beautifulsoup import BeautifulSoup
from lp.services.job.runner import JobRunner
+from lp.services.utils import seconds_since_epoch
from lp.services.webapp.publisher import canonical_url
from lp.testing import (
admin_logged_in,
@@ -145,7 +146,6 @@
authors = [self.factory.makePerson() for _ in range(5)]
with admin_logged_in():
author_emails = [author.preferredemail.email for author in authors]
- epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
dates = [
datetime(2015, 1, day + 1, tzinfo=pytz.UTC) for day in range(5)]
return [
@@ -155,12 +155,12 @@
"author": {
"name": authors[i].display_name,
"email": author_emails[i],
- "time": int((dates[i] - epoch).total_seconds()),
+ "time": int(seconds_since_epoch(dates[i])),
},
"committer": {
"name": authors[i].display_name,
"email": author_emails[i],
- "time": int((dates[i] - epoch).total_seconds()),
+ "time": int(seconds_since_epoch(dates[i])),
},
"parents": [unicode(hashlib.sha1(str(i - 1)).hexdigest())],
"tree": unicode(hashlib.sha1("").hexdigest()),
=== modified file 'lib/lp/code/model/gitref.py'
--- lib/lp/code/model/gitref.py 2018-08-23 12:34:24 +0000
+++ lib/lp/code/model/gitref.py 2018-09-27 13:59:29 +0000
@@ -9,7 +9,6 @@
'GitRefRemote',
]
-from datetime import datetime
from functools import partial
import json
import re
@@ -79,6 +78,7 @@
from lp.services.features import getFeatureFlag
from lp.services.memcache.interfaces import IMemcacheClient
from lp.services.timeout import urlfetch
+from lp.services.utils import seconds_since_epoch
from lp.services.webapp.interfaces import ILaunchBag
@@ -345,19 +345,18 @@
else:
# Fall back to synthesising something reasonable based on
# information in our own database.
- epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
log = [{
"sha1": self.commit_sha1,
"message": self.commit_message,
"author": None if self.author is None else {
"name": self.author.name_without_email,
"email": self.author.email,
- "time": (self.author_date - epoch).total_seconds(),
+ "time": seconds_since_epoch(self.author_date),
},
"committer": None if self.committer is None else {
"name": self.committer.name_without_email,
"email": self.committer.email,
- "time": (self.committer_date - epoch).total_seconds(),
+ "time": seconds_since_epoch(self.committer_date),
},
}]
return log
=== modified file 'lib/lp/code/model/tests/test_gitjob.py'
--- lib/lp/code/model/tests/test_gitjob.py 2017-10-04 01:29:35 +0000
+++ lib/lp/code/model/tests/test_gitjob.py 2018-09-27 13:59:29 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for `GitJob`s."""
@@ -44,6 +44,7 @@
from lp.services.database.constants import UTC_NOW
from lp.services.features.testing import FeatureFixture
from lp.services.job.runner import JobRunner
+from lp.services.utils import seconds_since_epoch
from lp.services.webapp import canonical_url
from lp.testing import (
TestCaseWithFactory,
@@ -97,7 +98,6 @@
@staticmethod
def makeFakeCommits(author, author_date_gen, paths):
- epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
dates = {path: next(author_date_gen) for path in paths}
return [{
"sha1": unicode(hashlib.sha1(path).hexdigest()),
@@ -105,12 +105,12 @@
"author": {
"name": author.displayname,
"email": author.preferredemail.email,
- "time": int((dates[path] - epoch).total_seconds()),
+ "time": int(seconds_since_epoch(dates[path])),
},
"committer": {
"name": author.displayname,
"email": author.preferredemail.email,
- "time": int((dates[path] - epoch).total_seconds()),
+ "time": int(seconds_since_epoch(dates[path])),
},
"parents": [],
"tree": unicode(hashlib.sha1("").hexdigest()),
=== modified file 'lib/lp/code/model/tests/test_gitref.py'
--- lib/lp/code/model/tests/test_gitref.py 2018-08-23 12:34:24 +0000
+++ lib/lp/code/model/tests/test_gitref.py 2018-09-27 13:59:29 +0000
@@ -42,6 +42,7 @@
from lp.services.config import config
from lp.services.features.testing import FeatureFixture
from lp.services.memcache.interfaces import IMemcacheClient
+from lp.services.utils import seconds_since_epoch
from lp.services.webapp.interfaces import OAuthPermission
from lp.testing import (
admin_logged_in,
@@ -134,7 +135,6 @@
with admin_logged_in():
self.author_emails = [
author.preferredemail.email for author in self.authors]
- epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
self.dates = [
datetime(2015, 1, 1, 0, 0, 0, tzinfo=pytz.UTC),
datetime(2015, 1, 2, 0, 0, 0, tzinfo=pytz.UTC),
@@ -148,12 +148,12 @@
"author": {
"name": self.authors[0].display_name,
"email": self.author_emails[0],
- "time": int((self.dates[1] - epoch).total_seconds()),
+ "time": int(seconds_since_epoch(self.dates[1])),
},
"committer": {
"name": self.authors[1].display_name,
"email": self.author_emails[1],
- "time": int((self.dates[1] - epoch).total_seconds()),
+ "time": int(seconds_since_epoch(self.dates[1])),
},
"parents": [self.sha1_root],
"tree": unicode(hashlib.sha1("").hexdigest()),
@@ -164,12 +164,12 @@
"author": {
"name": self.authors[1].display_name,
"email": self.author_emails[1],
- "time": int((self.dates[0] - epoch).total_seconds()),
+ "time": int(seconds_since_epoch(self.dates[0])),
},
"committer": {
"name": self.authors[0].display_name,
"email": self.author_emails[0],
- "time": int((self.dates[0] - epoch).total_seconds()),
+ "time": int(seconds_since_epoch(self.dates[0])),
},
"parents": [],
"tree": unicode(hashlib.sha1("").hexdigest()),
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2018-08-31 14:25:40 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2018-09-27 13:59:29 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for Git repositories."""
@@ -124,6 +124,7 @@
from lp.services.job.runner import JobRunner
from lp.services.mail import stub
from lp.services.propertycache import clear_property_cache
+from lp.services.utils import seconds_since_epoch
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.interfaces import OAuthPermission
from lp.testing import (
@@ -1283,7 +1284,6 @@
author = self.factory.makePerson()
with person_logged_in(author):
author_email = author.preferredemail.email
- epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
author_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
committer_date = datetime(2015, 1, 2, tzinfo=pytz.UTC)
hosting_fixture = self.useFixture(GitHostingFixture(commits=[
@@ -1293,12 +1293,12 @@
"author": {
"name": author.displayname,
"email": author_email,
- "time": int((author_date - epoch).total_seconds()),
+ "time": int(seconds_since_epoch(author_date)),
},
"committer": {
"name": "New Person",
"email": "new-person@xxxxxxxxxxx",
- "time": int((committer_date - epoch).total_seconds()),
+ "time": int(seconds_since_epoch(committer_date)),
},
"parents": [],
"tree": unicode(hashlib.sha1("").hexdigest()),
=== modified file 'lib/lp/code/stories/branches/xx-code-review-comments.txt'
--- lib/lp/code/stories/branches/xx-code-review-comments.txt 2018-05-13 10:35:52 +0000
+++ lib/lp/code/stories/branches/xx-code-review-comments.txt 2018-09-27 13:59:29 +0000
@@ -192,11 +192,11 @@
log entries first.
>>> from lp.code.tests.helpers import GitHostingFixture
+ >>> from lp.services.utils import seconds_since_epoch
>>> login('admin@xxxxxxxxxxxxx')
>>> bmp = factory.makeBranchMergeProposalForGit()
>>> bmp.requestReview(review_date)
- >>> epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
>>> commit_date = review_date + timedelta(days=1)
>>> hosting_fixture = GitHostingFixture()
>>> for i in range(2):
@@ -206,7 +206,7 @@
... u'author': {
... u'name': bmp.registrant.display_name,
... u'email': bmp.registrant.preferredemail.email,
- ... u'time': int((commit_date - epoch).total_seconds()),
+ ... u'time': int(seconds_since_epoch(commit_date)),
... },
... })
... hosting_fixture.getLog.result.insert(0, {
@@ -215,7 +215,7 @@
... u'author': {
... u'name': bmp.registrant.display_name,
... u'email': bmp.registrant.preferredemail.email,
- ... u'time': int((commit_date - epoch).total_seconds()),
+ ... u'time': int(seconds_since_epoch(commit_date)),
... },
... })
... commit_date += timedelta(days=1)
=== modified file 'lib/lp/services/tests/test_utils.py'
--- lib/lp/services/tests/test_utils.py 2018-04-17 09:41:46 +0000
+++ lib/lp/services/tests/test_utils.py 2018-09-27 13:59:29 +0000
@@ -37,6 +37,7 @@
run_capturing_output,
sanitise_urls,
save_bz2_pickle,
+ seconds_since_epoch,
traceback_info,
utc_now,
)
@@ -380,6 +381,18 @@
self.assertThat(now, LessThanOrEqual(new_now))
+class TestSecondsSinceEpoch(TestCase):
+ """Tests for `seconds_since_epoch`."""
+
+ def test_epoch(self):
+ epoch = datetime.fromtimestamp(0, tz=UTC)
+ self.assertEqual(0, seconds_since_epoch(epoch))
+
+ def test_start_of_2018(self):
+ dt = datetime(2018, 1, 1, tzinfo=UTC)
+ self.assertEqual(1514764800, seconds_since_epoch(dt))
+
+
class TestBZ2Pickle(TestCase):
"""Tests for `save_bz2_pickle` and `load_bz2_pickle`."""
=== modified file 'lib/lp/services/utils.py'
--- lib/lp/services/utils.py 2018-04-17 09:41:46 +0000
+++ lib/lp/services/utils.py 2018-09-27 13:59:29 +0000
@@ -25,6 +25,7 @@
'run_capturing_output',
'sanitise_urls',
'save_bz2_pickle',
+ 'seconds_since_epoch',
'text_delta',
'traceback_info',
'utc_now',
@@ -302,6 +303,14 @@
return datetime.now(tz=pytz.UTC)
+_epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
+
+
+def seconds_since_epoch(dt):
+ """Express a `datetime` as the number of seconds since the Unix epoch."""
+ return (dt - _epoch).total_seconds()
+
+
# This is a regular expression that matches email address embedded in
# text. It is not RFC 2821 compliant, nor does it need to be. This
# expression strives to identify probable email addresses so that they
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2018-09-13 15:21:05 +0000
+++ lib/lp/snappy/browser/snap.py 2018-09-27 13:59:29 +0000
@@ -57,7 +57,9 @@
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.services.features import getFeatureFlag
from lp.services.helpers import english_list
+from lp.services.propertycache import cachedproperty
from lp.services.scripts import log
+from lp.services.utils import seconds_since_epoch
from lp.services.webapp import (
canonical_url,
ContextMenu,
@@ -178,9 +180,9 @@
class SnapView(LaunchpadView):
"""Default view of a Snap."""
- @property
- def builds(self):
- return builds_for_snap(self.context)
+ @cachedproperty
+ def builds_and_requests(self):
+ return builds_and_requests_for_snap(self.context)
@property
def person_picker(self):
@@ -209,23 +211,47 @@
return ', '.join(self.context.store_channels)
-def builds_for_snap(snap):
- """A list of interesting builds.
+def builds_and_requests_for_snap(snap):
+ """A list of interesting builds and build requests.
- All pending builds are shown, as well as 1-10 recent builds. Recent
- builds are ordered by date finished (if completed) or date_started (if
- date finished is not set due to an error building or other circumstance
- which resulted in the build not being completed). This allows started
- but unfinished builds to show up in the view but be discarded as more
- recent builds become available.
+ All pending builds and pending build requests are shown, as well as up
+ to 10 recent builds and recent failed build requests. Pending items are
+ ordered by the date they were created; recent items are ordered by the
+ date they finished (if available) or the date they started (if the date
+ they finished is not set due to an error). This allows started but
+ unfinished builds to show up in the view but be discarded as more recent
+ builds become available.
Builds that the user does not have permission to see are excluded (by
the model code).
"""
- builds = list(snap.pending_builds)
- if len(builds) < 10:
- builds.extend(snap.completed_builds[:10 - len(builds)])
- return builds
+ # We need to interleave items of different types, so SQL can't do all
+ # the sorting for us.
+ def make_sort_key(*date_attrs):
+ def _sort_key(item):
+ for date_attr in date_attrs:
+ if getattr(item, date_attr, None) is not None:
+ return -seconds_since_epoch(getattr(item, date_attr))
+ return 0
+
+ return _sort_key
+
+ items = sorted(
+ list(snap.pending_builds) + list(snap.pending_build_requests),
+ key=make_sort_key("date_created", "date_requested"))
+ if len(items) < 10:
+ # We need to interleave two unbounded result sets, but we only need
+ # enough items from them to make the total count up to 10. It's
+ # simplest to just fetch the upper bound from each set and do our
+ # own sorting.
+ recent_items = sorted(
+ list(snap.completed_builds[:10 - len(items)]) +
+ list(snap.failed_build_requests[:10 - len(items)]),
+ key=make_sort_key(
+ "date_finished", "date_started",
+ "date_created", "date_requested"))
+ items.extend(recent_items[:10 - len(items)])
+ return items
def new_builds_notification_text(builds, already_pending=None):
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2018-09-13 15:21:05 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2018-09-27 13:59:29 +0000
@@ -29,6 +29,7 @@
AfterPreprocessing,
Equals,
Is,
+ MatchesListwise,
MatchesSetwise,
MatchesStructure,
)
@@ -56,6 +57,8 @@
from lp.services.config import config
from lp.services.database.constants import UTC_NOW
from lp.services.features.testing import FeatureFixture
+from lp.services.job.interfaces.job import JobStatus
+from lp.services.propertycache import get_property_cache
from lp.services.webapp import canonical_url
from lp.services.webapp.servers import LaunchpadTestRequest
from lp.snappy.browser.snap import (
@@ -1344,7 +1347,7 @@
"This snap package has not been built yet.",
self.getMainText(snap))
- def test_index_pending(self):
+ def test_index_pending_build(self):
# A pending build is listed as such.
build = self.makeBuild()
build.queueBuild()
@@ -1355,6 +1358,38 @@
Primary Archive for Ubuntu Linux
""", self.getMainText(build.snap))
+ def test_index_pending_build_request(self):
+ # A pending build request is listed as such.
+ snap = self.makeSnap()
+ with person_logged_in(snap.owner):
+ snap.requestBuilds(
+ snap.owner, snap.distro_series.main_archive,
+ PackagePublishingPocket.UPDATES)
+ self.assertTextMatchesExpressionIgnoreWhitespace("""\
+ Latest builds
+ Status When complete Architecture Archive
+ Pending build request
+ Primary Archive for Ubuntu Linux
+ """, self.getMainText(snap))
+
+ def test_index_failed_build_request(self):
+ # A failed build request is listed as such, with its error message.
+ snap = self.makeSnap()
+ with person_logged_in(snap.owner):
+ request = snap.requestBuilds(
+ snap.owner, snap.distro_series.main_archive,
+ PackagePublishingPocket.UPDATES)
+ job = removeSecurityProxy(removeSecurityProxy(request)._job)
+ job.job._status = JobStatus.FAILED
+ job.job.date_finished = datetime.now(pytz.UTC) - timedelta(hours=1)
+ job.error_message = "Boom"
+ self.assertTextMatchesExpressionIgnoreWhitespace("""\
+ Latest builds
+ Status When complete Architecture Archive
+ Failed build request 1 hour ago \(Boom\)
+ Primary Archive for Ubuntu Linux
+ """, self.getMainText(snap))
+
def test_index_store_upload(self):
# If the snap package is to be automatically uploaded to the store,
# the index page shows details of this.
@@ -1382,8 +1417,8 @@
build.updateStatus(
status, date_finished=build.date_started + timedelta(minutes=30))
- def test_builds(self):
- # SnapView.builds produces reasonable results.
+ def test_builds_and_requests(self):
+ # SnapView.builds_and_requests produces reasonable results.
snap = self.makeSnap()
# Create oldest builds first so that they sort properly by id.
date_gen = time_counter(
@@ -1392,16 +1427,67 @@
self.makeBuild(snap=snap, date_created=next(date_gen))
for i in range(11)]
view = SnapView(snap, None)
- self.assertEqual(list(reversed(builds)), view.builds)
+ self.assertEqual(list(reversed(builds)), view.builds_and_requests)
self.setStatus(builds[10], BuildStatus.FULLYBUILT)
self.setStatus(builds[9], BuildStatus.FAILEDTOBUILD)
+ del get_property_cache(view).builds_and_requests
# When there are >= 9 pending builds, only the most recent of any
# completed builds is returned.
self.assertEqual(
- list(reversed(builds[:9])) + [builds[10]], view.builds)
+ list(reversed(builds[:9])) + [builds[10]],
+ view.builds_and_requests)
for build in builds[:9]:
self.setStatus(build, BuildStatus.FULLYBUILT)
- self.assertEqual(list(reversed(builds[1:])), view.builds)
+ del get_property_cache(view).builds_and_requests
+ self.assertEqual(list(reversed(builds[1:])), view.builds_and_requests)
+
+ def test_builds_and_requests_shows_build_requests(self):
+ # SnapView.builds_and_requests interleaves build requests with
+ # builds.
+ snap = self.makeSnap()
+ date_gen = time_counter(
+ datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1))
+ builds = [
+ self.makeBuild(snap=snap, date_created=next(date_gen))
+ for i in range(3)]
+ self.setStatus(builds[2], BuildStatus.FULLYBUILT)
+ with person_logged_in(snap.owner):
+ request = snap.requestBuilds(
+ snap.owner, snap.distro_series.main_archive,
+ PackagePublishingPocket.UPDATES)
+ job = removeSecurityProxy(removeSecurityProxy(request)._job)
+ job.job.date_created = next(date_gen)
+ view = SnapView(snap, None)
+ # The pending build request is interleaved in date order with
+ # pending builds, and these are followed by completed builds.
+ self.assertThat(view.builds_and_requests, MatchesListwise([
+ MatchesStructure.byEquality(id=request.id),
+ Equals(builds[1]),
+ Equals(builds[0]),
+ Equals(builds[2]),
+ ]))
+ transaction.commit()
+ builds.append(self.makeBuild(snap=snap))
+ del get_property_cache(view).builds_and_requests
+ self.assertThat(view.builds_and_requests, MatchesListwise([
+ Equals(builds[3]),
+ MatchesStructure.byEquality(id=request.id),
+ Equals(builds[1]),
+ Equals(builds[0]),
+ Equals(builds[2]),
+ ]))
+ # If we pretend that the job failed, it is still listed, but after
+ # any pending builds.
+ job.job._status = JobStatus.FAILED
+ job.job.date_finished = job.date_created + timedelta(minutes=30)
+ del get_property_cache(view).builds_and_requests
+ self.assertThat(view.builds_and_requests, MatchesListwise([
+ Equals(builds[3]),
+ Equals(builds[1]),
+ Equals(builds[0]),
+ MatchesStructure.byEquality(id=request.id),
+ Equals(builds[2]),
+ ]))
def test_store_channels_empty(self):
snap = self.factory.makeSnap()
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2018-09-13 15:21:05 +0000
+++ lib/lp/snappy/interfaces/snap.py 2018-09-27 13:59:29 +0000
@@ -459,6 +459,11 @@
value_type=Reference(ISnapBuildRequest),
required=True, readonly=True)))
+ failed_build_requests = exported(doNotSnapshot(CollectionField(
+ title=_("Failed build requests for this snap package."),
+ value_type=Reference(ISnapBuildRequest),
+ required=True, readonly=True)))
+
# XXX cjwatson 2018-06-20: Deprecated as an exported method; can become
# an internal helper method once production JavaScript no longer uses
# it.
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2018-09-13 15:21:05 +0000
+++ lib/lp/snappy/model/snap.py 2018-09-27 13:59:29 +0000
@@ -676,6 +676,14 @@
self, statuses=(JobStatus.WAITING, JobStatus.RUNNING))
return [SnapBuildRequest.fromJob(job) for job in jobs]
+ @property
+ def failed_build_requests(self):
+ """See `ISnap`."""
+ job_source = getUtility(ISnapRequestBuildsJobSource)
+ # The returned jobs are ordered by descending ID.
+ jobs = job_source.findBySnap(self, statuses=(JobStatus.FAILED,))
+ return [SnapBuildRequest.fromJob(job) for job in jobs]
+
def _getBuilds(self, filter_term, order_by):
"""The actual query to get the builds."""
query_args = [
=== modified file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt 2018-09-13 15:21:05 +0000
+++ lib/lp/snappy/templates/snap-index.pt 2018-09-27 13:59:29 +0000
@@ -156,51 +156,62 @@
</tr>
</thead>
<tbody>
- <tal:snap-build-requests repeat="request context/pending_build_requests">
- <tr tal:attributes="id string:request-${request/id}">
- <td colspan="3"
- tal:attributes="class string:request_status ${request/status/name}">
- <span tal:replace="structure request/image:icon"/>
- <tal:title replace="request/status/title"/> build request
- </td>
- <td>
- <tal:archive replace="structure request/archive/fmt:link"/>
- </td>
- </tr>
- </tal:snap-build-requests>
- <tal:snap-builds repeat="build view/builds">
- <tr 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"/>
- </td>
- <td class="datebuilt">
- <tal:date replace="build/date/fmt:displaydate"/>
- <tal:estimate condition="build/estimate">
- (estimated)
- </tal:estimate>
+ <tal:snap-builds-and-requests repeat="item view/builds_and_requests">
+ <tal:snap-build-request condition="item/date_requested|nothing">
+ <tr tal:define="request item"
+ tal:attributes="id string:request-${request/id}">
+ <td tal:attributes="class string:request_status ${request/status/name}">
+ <span tal:replace="structure request/image:icon"/>
+ <tal:title replace="request/status/title"/> build request
+ </td>
+ <td>
+ <tal:date condition="request/date_finished"
+ replace="request/date_finished/fmt:displaydate"/>
+ <tal:error-message condition="request/error_message">
+ (<span tal:replace="request/error_message"/>)
+ </tal:error-message>
+ </td>
+ <td/>
+ <td>
+ <tal:archive replace="structure request/archive/fmt:link"/>
+ </td>
+ </tr>
+ </tal:snap-build-request>
+ <tal:snap-build condition="not: item/date_requested|nothing">
+ <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"/>
+ </td>
+ <td class="datebuilt">
+ <tal:date replace="build/date/fmt:displaydate"/>
+ <tal:estimate condition="build/estimate">
+ (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>
- <td>
- <a class="sprite distribution"
- tal:define="archseries build/distro_arch_series"
- tal:attributes="href archseries/fmt:url"
- tal:content="archseries/architecturetag"/>
- </td>
- <td>
- <tal:archive replace="structure build/archive/fmt:link"/>
- </td>
- </tr>
- </tal:snap-builds>
+ <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>
+ <td>
+ <a class="sprite distribution"
+ tal:define="archseries build/distro_arch_series"
+ tal:attributes="href archseries/fmt:url"
+ tal:content="archseries/architecturetag"/>
+ </td>
+ <td>
+ <tal:archive replace="structure build/archive/fmt:link"/>
+ </td>
+ </tr>
+ </tal:snap-build>
+ </tal:snap-builds-and-requests>
</tbody>
</table>
- <p tal:condition="not: view/builds">
+ <p tal:condition="not: view/builds_and_requests">
This snap package has not been built yet.
</p>
<div tal:define="link context/menu:context/request_builds"
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2018-09-13 15:21:05 +0000
+++ lib/lp/snappy/tests/test_snap.py 2018-09-27 13:59:29 +0000
@@ -177,7 +177,7 @@
self.assertThat(
self.factory.makeSnap(),
DoesNotSnapshot(
- ["pending_build_requests",
+ ["pending_build_requests", "failed_build_requests",
"builds", "completed_builds", "pending_builds"],
ISnapView))
Follow ups