Merge lp:~cjwatson/launchpad/snap-failed-build-requests into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18789
Proposed branch: lp:~cjwatson/launchpad/snap-failed-build-requests
Merge into: lp:launchpad
Diff against target: 745 lines (+250/-92)
15 files modified
lib/lp/code/browser/tests/test_branchmergeproposal.py (+3/-4)
lib/lp/code/browser/tests/test_gitref.py (+3/-3)
lib/lp/code/model/gitref.py (+3/-4)
lib/lp/code/model/tests/test_gitjob.py (+4/-4)
lib/lp/code/model/tests/test_gitref.py (+5/-5)
lib/lp/code/model/tests/test_gitrepository.py (+4/-4)
lib/lp/code/stories/branches/xx-code-review-comments.txt (+3/-3)
lib/lp/services/tests/test_utils.py (+13/-0)
lib/lp/services/utils.py (+9/-0)
lib/lp/snappy/browser/snap.py (+41/-15)
lib/lp/snappy/browser/tests/test_snap.py (+92/-6)
lib/lp/snappy/interfaces/snap.py (+5/-0)
lib/lp/snappy/model/snap.py (+11/-1)
lib/lp/snappy/templates/snap-index.pt (+53/-42)
lib/lp/snappy/tests/test_snap.py (+1/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-failed-build-requests
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+355773@code.launchpad.net

Commit message

Show recent failed build requests on Snap:+index.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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-10-03 00:52:37 +0000
@@ -93,6 +93,7 @@
93from lp.services.librarian.interfaces.client import LibrarianServerError93from lp.services.librarian.interfaces.client import LibrarianServerError
94from lp.services.messages.model.message import MessageSet94from lp.services.messages.model.message import MessageSet
95from lp.services.timeout import TimeoutError95from lp.services.timeout import TimeoutError
96from lp.services.utils import seconds_since_epoch
96from lp.services.webapp import canonical_url97from lp.services.webapp import canonical_url
97from lp.services.webapp.interfaces import BrowserNotificationLevel98from lp.services.webapp.interfaces import BrowserNotificationLevel
98from lp.services.webapp.servers import LaunchpadTestRequest99from lp.services.webapp.servers import LaunchpadTestRequest
@@ -1515,7 +1516,6 @@
1515 author = self.factory.makePerson()1516 author = self.factory.makePerson()
1516 with person_logged_in(author):1517 with person_logged_in(author):
1517 author_email = author.preferredemail.email1518 author_email = author.preferredemail.email
1518 epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
1519 review_date = self.factory.getUniqueDate()1519 review_date = self.factory.getUniqueDate()
1520 commit_date = self.factory.getUniqueDate()1520 commit_date = self.factory.getUniqueDate()
1521 bmp = self.factory.makeBranchMergeProposalForGit(1521 bmp = self.factory.makeBranchMergeProposalForGit(
@@ -1527,7 +1527,7 @@
1527 'author': {1527 'author': {
1528 'name': author.display_name,1528 'name': author.display_name,
1529 'email': author_email,1529 'email': author_email,
1530 'time': int((commit_date - epoch).total_seconds()),1530 'time': int(seconds_since_epoch(commit_date)),
1531 },1531 },
1532 }1532 }
1533 ]))1533 ]))
@@ -1614,7 +1614,6 @@
1614 # SHA-1 and can ask the repository for its unmerged commits.1614 # SHA-1 and can ask the repository for its unmerged commits.
1615 bmp = self.factory.makeBranchMergeProposalForGit()1615 bmp = self.factory.makeBranchMergeProposalForGit()
1616 sha1 = unicode(hashlib.sha1(b'0').hexdigest())1616 sha1 = unicode(hashlib.sha1(b'0').hexdigest())
1617 epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
1618 commit_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)1617 commit_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
1619 self.useFixture(GitHostingFixture(log=[1618 self.useFixture(GitHostingFixture(log=[
1620 {1619 {
@@ -1623,7 +1622,7 @@
1623 'author': {1622 'author': {
1624 'name': 'Example Person',1623 'name': 'Example Person',
1625 'email': 'person@example.org',1624 'email': 'person@example.org',
1626 'time': int((commit_date - epoch).total_seconds()),1625 'time': int(seconds_since_epoch(commit_date)),
1627 },1626 },
1628 }1627 }
1629 ]))1628 ]))
16301629
=== 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-10-03 00:52:37 +0000
@@ -23,6 +23,7 @@
23from lp.code.tests.helpers import GitHostingFixture23from lp.code.tests.helpers import GitHostingFixture
24from lp.services.beautifulsoup import BeautifulSoup24from lp.services.beautifulsoup import BeautifulSoup
25from lp.services.job.runner import JobRunner25from lp.services.job.runner import JobRunner
26from lp.services.utils import seconds_since_epoch
26from lp.services.webapp.publisher import canonical_url27from lp.services.webapp.publisher import canonical_url
27from lp.testing import (28from lp.testing import (
28 admin_logged_in,29 admin_logged_in,
@@ -145,7 +146,6 @@
145 authors = [self.factory.makePerson() for _ in range(5)]146 authors = [self.factory.makePerson() for _ in range(5)]
146 with admin_logged_in():147 with admin_logged_in():
147 author_emails = [author.preferredemail.email for author in authors]148 author_emails = [author.preferredemail.email for author in authors]
148 epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
149 dates = [149 dates = [
150 datetime(2015, 1, day + 1, tzinfo=pytz.UTC) for day in range(5)]150 datetime(2015, 1, day + 1, tzinfo=pytz.UTC) for day in range(5)]
151 return [151 return [
@@ -155,12 +155,12 @@
155 "author": {155 "author": {
156 "name": authors[i].display_name,156 "name": authors[i].display_name,
157 "email": author_emails[i],157 "email": author_emails[i],
158 "time": int((dates[i] - epoch).total_seconds()),158 "time": int(seconds_since_epoch(dates[i])),
159 },159 },
160 "committer": {160 "committer": {
161 "name": authors[i].display_name,161 "name": authors[i].display_name,
162 "email": author_emails[i],162 "email": author_emails[i],
163 "time": int((dates[i] - epoch).total_seconds()),163 "time": int(seconds_since_epoch(dates[i])),
164 },164 },
165 "parents": [unicode(hashlib.sha1(str(i - 1)).hexdigest())],165 "parents": [unicode(hashlib.sha1(str(i - 1)).hexdigest())],
166 "tree": unicode(hashlib.sha1("").hexdigest()),166 "tree": unicode(hashlib.sha1("").hexdigest()),
167167
=== 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-10-03 00:52:37 +0000
@@ -9,7 +9,6 @@
9 'GitRefRemote',9 'GitRefRemote',
10 ]10 ]
1111
12from datetime import datetime
13from functools import partial12from functools import partial
14import json13import json
15import re14import re
@@ -79,6 +78,7 @@
79from lp.services.features import getFeatureFlag78from lp.services.features import getFeatureFlag
80from lp.services.memcache.interfaces import IMemcacheClient79from lp.services.memcache.interfaces import IMemcacheClient
81from lp.services.timeout import urlfetch80from lp.services.timeout import urlfetch
81from lp.services.utils import seconds_since_epoch
82from lp.services.webapp.interfaces import ILaunchBag82from lp.services.webapp.interfaces import ILaunchBag
8383
8484
@@ -345,19 +345,18 @@
345 else:345 else:
346 # Fall back to synthesising something reasonable based on346 # Fall back to synthesising something reasonable based on
347 # information in our own database.347 # information in our own database.
348 epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
349 log = [{348 log = [{
350 "sha1": self.commit_sha1,349 "sha1": self.commit_sha1,
351 "message": self.commit_message,350 "message": self.commit_message,
352 "author": None if self.author is None else {351 "author": None if self.author is None else {
353 "name": self.author.name_without_email,352 "name": self.author.name_without_email,
354 "email": self.author.email,353 "email": self.author.email,
355 "time": (self.author_date - epoch).total_seconds(),354 "time": seconds_since_epoch(self.author_date),
356 },355 },
357 "committer": None if self.committer is None else {356 "committer": None if self.committer is None else {
358 "name": self.committer.name_without_email,357 "name": self.committer.name_without_email,
359 "email": self.committer.email,358 "email": self.committer.email,
360 "time": (self.committer_date - epoch).total_seconds(),359 "time": seconds_since_epoch(self.committer_date),
361 },360 },
362 }]361 }]
363 return log362 return log
364363
=== 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-10-03 00:52:37 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015-2017 Canonical Ltd. This software is licensed under the1# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for `GitJob`s."""4"""Tests for `GitJob`s."""
@@ -44,6 +44,7 @@
44from lp.services.database.constants import UTC_NOW44from lp.services.database.constants import UTC_NOW
45from lp.services.features.testing import FeatureFixture45from lp.services.features.testing import FeatureFixture
46from lp.services.job.runner import JobRunner46from lp.services.job.runner import JobRunner
47from lp.services.utils import seconds_since_epoch
47from lp.services.webapp import canonical_url48from lp.services.webapp import canonical_url
48from lp.testing import (49from lp.testing import (
49 TestCaseWithFactory,50 TestCaseWithFactory,
@@ -97,7 +98,6 @@
9798
98 @staticmethod99 @staticmethod
99 def makeFakeCommits(author, author_date_gen, paths):100 def makeFakeCommits(author, author_date_gen, paths):
100 epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
101 dates = {path: next(author_date_gen) for path in paths}101 dates = {path: next(author_date_gen) for path in paths}
102 return [{102 return [{
103 "sha1": unicode(hashlib.sha1(path).hexdigest()),103 "sha1": unicode(hashlib.sha1(path).hexdigest()),
@@ -105,12 +105,12 @@
105 "author": {105 "author": {
106 "name": author.displayname,106 "name": author.displayname,
107 "email": author.preferredemail.email,107 "email": author.preferredemail.email,
108 "time": int((dates[path] - epoch).total_seconds()),108 "time": int(seconds_since_epoch(dates[path])),
109 },109 },
110 "committer": {110 "committer": {
111 "name": author.displayname,111 "name": author.displayname,
112 "email": author.preferredemail.email,112 "email": author.preferredemail.email,
113 "time": int((dates[path] - epoch).total_seconds()),113 "time": int(seconds_since_epoch(dates[path])),
114 },114 },
115 "parents": [],115 "parents": [],
116 "tree": unicode(hashlib.sha1("").hexdigest()),116 "tree": unicode(hashlib.sha1("").hexdigest()),
117117
=== 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-10-03 00:52:37 +0000
@@ -42,6 +42,7 @@
42from lp.services.config import config42from lp.services.config import config
43from lp.services.features.testing import FeatureFixture43from lp.services.features.testing import FeatureFixture
44from lp.services.memcache.interfaces import IMemcacheClient44from lp.services.memcache.interfaces import IMemcacheClient
45from lp.services.utils import seconds_since_epoch
45from lp.services.webapp.interfaces import OAuthPermission46from lp.services.webapp.interfaces import OAuthPermission
46from lp.testing import (47from lp.testing import (
47 admin_logged_in,48 admin_logged_in,
@@ -134,7 +135,6 @@
134 with admin_logged_in():135 with admin_logged_in():
135 self.author_emails = [136 self.author_emails = [
136 author.preferredemail.email for author in self.authors]137 author.preferredemail.email for author in self.authors]
137 epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
138 self.dates = [138 self.dates = [
139 datetime(2015, 1, 1, 0, 0, 0, tzinfo=pytz.UTC),139 datetime(2015, 1, 1, 0, 0, 0, tzinfo=pytz.UTC),
140 datetime(2015, 1, 2, 0, 0, 0, tzinfo=pytz.UTC),140 datetime(2015, 1, 2, 0, 0, 0, tzinfo=pytz.UTC),
@@ -148,12 +148,12 @@
148 "author": {148 "author": {
149 "name": self.authors[0].display_name,149 "name": self.authors[0].display_name,
150 "email": self.author_emails[0],150 "email": self.author_emails[0],
151 "time": int((self.dates[1] - epoch).total_seconds()),151 "time": int(seconds_since_epoch(self.dates[1])),
152 },152 },
153 "committer": {153 "committer": {
154 "name": self.authors[1].display_name,154 "name": self.authors[1].display_name,
155 "email": self.author_emails[1],155 "email": self.author_emails[1],
156 "time": int((self.dates[1] - epoch).total_seconds()),156 "time": int(seconds_since_epoch(self.dates[1])),
157 },157 },
158 "parents": [self.sha1_root],158 "parents": [self.sha1_root],
159 "tree": unicode(hashlib.sha1("").hexdigest()),159 "tree": unicode(hashlib.sha1("").hexdigest()),
@@ -164,12 +164,12 @@
164 "author": {164 "author": {
165 "name": self.authors[1].display_name,165 "name": self.authors[1].display_name,
166 "email": self.author_emails[1],166 "email": self.author_emails[1],
167 "time": int((self.dates[0] - epoch).total_seconds()),167 "time": int(seconds_since_epoch(self.dates[0])),
168 },168 },
169 "committer": {169 "committer": {
170 "name": self.authors[0].display_name,170 "name": self.authors[0].display_name,
171 "email": self.author_emails[0],171 "email": self.author_emails[0],
172 "time": int((self.dates[0] - epoch).total_seconds()),172 "time": int(seconds_since_epoch(self.dates[0])),
173 },173 },
174 "parents": [],174 "parents": [],
175 "tree": unicode(hashlib.sha1("").hexdigest()),175 "tree": unicode(hashlib.sha1("").hexdigest()),
176176
=== 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-10-03 00:52:37 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015-2017 Canonical Ltd. This software is licensed under the1# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for Git repositories."""4"""Tests for Git repositories."""
@@ -124,6 +124,7 @@
124from lp.services.job.runner import JobRunner124from lp.services.job.runner import JobRunner
125from lp.services.mail import stub125from lp.services.mail import stub
126from lp.services.propertycache import clear_property_cache126from lp.services.propertycache import clear_property_cache
127from lp.services.utils import seconds_since_epoch
127from lp.services.webapp.authorization import check_permission128from lp.services.webapp.authorization import check_permission
128from lp.services.webapp.interfaces import OAuthPermission129from lp.services.webapp.interfaces import OAuthPermission
129from lp.testing import (130from lp.testing import (
@@ -1283,7 +1284,6 @@
1283 author = self.factory.makePerson()1284 author = self.factory.makePerson()
1284 with person_logged_in(author):1285 with person_logged_in(author):
1285 author_email = author.preferredemail.email1286 author_email = author.preferredemail.email
1286 epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
1287 author_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)1287 author_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
1288 committer_date = datetime(2015, 1, 2, tzinfo=pytz.UTC)1288 committer_date = datetime(2015, 1, 2, tzinfo=pytz.UTC)
1289 hosting_fixture = self.useFixture(GitHostingFixture(commits=[1289 hosting_fixture = self.useFixture(GitHostingFixture(commits=[
@@ -1293,12 +1293,12 @@
1293 "author": {1293 "author": {
1294 "name": author.displayname,1294 "name": author.displayname,
1295 "email": author_email,1295 "email": author_email,
1296 "time": int((author_date - epoch).total_seconds()),1296 "time": int(seconds_since_epoch(author_date)),
1297 },1297 },
1298 "committer": {1298 "committer": {
1299 "name": "New Person",1299 "name": "New Person",
1300 "email": "new-person@example.org",1300 "email": "new-person@example.org",
1301 "time": int((committer_date - epoch).total_seconds()),1301 "time": int(seconds_since_epoch(committer_date)),
1302 },1302 },
1303 "parents": [],1303 "parents": [],
1304 "tree": unicode(hashlib.sha1("").hexdigest()),1304 "tree": unicode(hashlib.sha1("").hexdigest()),
13051305
=== 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-10-03 00:52:37 +0000
@@ -192,11 +192,11 @@
192log entries first.192log entries first.
193193
194 >>> from lp.code.tests.helpers import GitHostingFixture194 >>> from lp.code.tests.helpers import GitHostingFixture
195 >>> from lp.services.utils import seconds_since_epoch
195196
196 >>> login('admin@canonical.com')197 >>> login('admin@canonical.com')
197 >>> bmp = factory.makeBranchMergeProposalForGit()198 >>> bmp = factory.makeBranchMergeProposalForGit()
198 >>> bmp.requestReview(review_date)199 >>> bmp.requestReview(review_date)
199 >>> epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
200 >>> commit_date = review_date + timedelta(days=1)200 >>> commit_date = review_date + timedelta(days=1)
201 >>> hosting_fixture = GitHostingFixture()201 >>> hosting_fixture = GitHostingFixture()
202 >>> for i in range(2):202 >>> for i in range(2):
@@ -206,7 +206,7 @@
206 ... u'author': {206 ... u'author': {
207 ... u'name': bmp.registrant.display_name,207 ... u'name': bmp.registrant.display_name,
208 ... u'email': bmp.registrant.preferredemail.email,208 ... u'email': bmp.registrant.preferredemail.email,
209 ... u'time': int((commit_date - epoch).total_seconds()),209 ... u'time': int(seconds_since_epoch(commit_date)),
210 ... },210 ... },
211 ... })211 ... })
212 ... hosting_fixture.getLog.result.insert(0, {212 ... hosting_fixture.getLog.result.insert(0, {
@@ -215,7 +215,7 @@
215 ... u'author': {215 ... u'author': {
216 ... u'name': bmp.registrant.display_name,216 ... u'name': bmp.registrant.display_name,
217 ... u'email': bmp.registrant.preferredemail.email,217 ... u'email': bmp.registrant.preferredemail.email,
218 ... u'time': int((commit_date - epoch).total_seconds()),218 ... u'time': int(seconds_since_epoch(commit_date)),
219 ... },219 ... },
220 ... })220 ... })
221 ... commit_date += timedelta(days=1)221 ... commit_date += timedelta(days=1)
222222
=== 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-10-03 00:52:37 +0000
@@ -37,6 +37,7 @@
37 run_capturing_output,37 run_capturing_output,
38 sanitise_urls,38 sanitise_urls,
39 save_bz2_pickle,39 save_bz2_pickle,
40 seconds_since_epoch,
40 traceback_info,41 traceback_info,
41 utc_now,42 utc_now,
42 )43 )
@@ -380,6 +381,18 @@
380 self.assertThat(now, LessThanOrEqual(new_now))381 self.assertThat(now, LessThanOrEqual(new_now))
381382
382383
384class TestSecondsSinceEpoch(TestCase):
385 """Tests for `seconds_since_epoch`."""
386
387 def test_epoch(self):
388 epoch = datetime.fromtimestamp(0, tz=UTC)
389 self.assertEqual(0, seconds_since_epoch(epoch))
390
391 def test_start_of_2018(self):
392 dt = datetime(2018, 1, 1, tzinfo=UTC)
393 self.assertEqual(1514764800, seconds_since_epoch(dt))
394
395
383class TestBZ2Pickle(TestCase):396class TestBZ2Pickle(TestCase):
384 """Tests for `save_bz2_pickle` and `load_bz2_pickle`."""397 """Tests for `save_bz2_pickle` and `load_bz2_pickle`."""
385398
386399
=== 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-10-03 00:52:37 +0000
@@ -25,6 +25,7 @@
25 'run_capturing_output',25 'run_capturing_output',
26 'sanitise_urls',26 'sanitise_urls',
27 'save_bz2_pickle',27 'save_bz2_pickle',
28 'seconds_since_epoch',
28 'text_delta',29 'text_delta',
29 'traceback_info',30 'traceback_info',
30 'utc_now',31 'utc_now',
@@ -302,6 +303,14 @@
302 return datetime.now(tz=pytz.UTC)303 return datetime.now(tz=pytz.UTC)
303304
304305
306_epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
307
308
309def seconds_since_epoch(dt):
310 """Express a `datetime` as the number of seconds since the Unix epoch."""
311 return (dt - _epoch).total_seconds()
312
313
305# This is a regular expression that matches email address embedded in314# This is a regular expression that matches email address embedded in
306# text. It is not RFC 2821 compliant, nor does it need to be. This315# text. It is not RFC 2821 compliant, nor does it need to be. This
307# expression strives to identify probable email addresses so that they316# expression strives to identify probable email addresses so that they
308317
=== 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-10-03 00:52:37 +0000
@@ -57,7 +57,9 @@
57from lp.registry.interfaces.pocket import PackagePublishingPocket57from lp.registry.interfaces.pocket import PackagePublishingPocket
58from lp.services.features import getFeatureFlag58from lp.services.features import getFeatureFlag
59from lp.services.helpers import english_list59from lp.services.helpers import english_list
60from lp.services.propertycache import cachedproperty
60from lp.services.scripts import log61from lp.services.scripts import log
62from lp.services.utils import seconds_since_epoch
61from lp.services.webapp import (63from lp.services.webapp import (
62 canonical_url,64 canonical_url,
63 ContextMenu,65 ContextMenu,
@@ -178,9 +180,9 @@
178class SnapView(LaunchpadView):180class SnapView(LaunchpadView):
179 """Default view of a Snap."""181 """Default view of a Snap."""
180182
181 @property183 @cachedproperty
182 def builds(self):184 def builds_and_requests(self):
183 return builds_for_snap(self.context)185 return builds_and_requests_for_snap(self.context)
184186
185 @property187 @property
186 def person_picker(self):188 def person_picker(self):
@@ -209,23 +211,47 @@
209 return ', '.join(self.context.store_channels)211 return ', '.join(self.context.store_channels)
210212
211213
212def builds_for_snap(snap):214def builds_and_requests_for_snap(snap):
213 """A list of interesting builds.215 """A list of interesting builds and build requests.
214216
215 All pending builds are shown, as well as 1-10 recent builds. Recent217 All pending builds and pending build requests are shown, as well as up
216 builds are ordered by date finished (if completed) or date_started (if218 to 10 recent builds and recent failed build requests. Pending items are
217 date finished is not set due to an error building or other circumstance219 ordered by the date they were created; recent items are ordered by the
218 which resulted in the build not being completed). This allows started220 date they finished (if available) or the date they started (if the date
219 but unfinished builds to show up in the view but be discarded as more221 they finished is not set due to an error). This allows started but
220 recent builds become available.222 unfinished builds to show up in the view but be discarded as more recent
223 builds become available.
221224
222 Builds that the user does not have permission to see are excluded (by225 Builds that the user does not have permission to see are excluded (by
223 the model code).226 the model code).
224 """227 """
225 builds = list(snap.pending_builds)228 # We need to interleave items of different types, so SQL can't do all
226 if len(builds) < 10:229 # the sorting for us.
227 builds.extend(snap.completed_builds[:10 - len(builds)])230 def make_sort_key(*date_attrs):
228 return builds231 def _sort_key(item):
232 for date_attr in date_attrs:
233 if getattr(item, date_attr, None) is not None:
234 return -seconds_since_epoch(getattr(item, date_attr))
235 return 0
236
237 return _sort_key
238
239 items = sorted(
240 list(snap.pending_builds) + list(snap.pending_build_requests),
241 key=make_sort_key("date_created", "date_requested"))
242 if len(items) < 10:
243 # We need to interleave two unbounded result sets, but we only need
244 # enough items from them to make the total count up to 10. It's
245 # simplest to just fetch the upper bound from each set and do our
246 # own sorting.
247 recent_items = sorted(
248 list(snap.completed_builds[:10 - len(items)]) +
249 list(snap.failed_build_requests[:10 - len(items)]),
250 key=make_sort_key(
251 "date_finished", "date_started",
252 "date_created", "date_requested"))
253 items.extend(recent_items[:10 - len(items)])
254 return items
229255
230256
231def new_builds_notification_text(builds, already_pending=None):257def new_builds_notification_text(builds, already_pending=None):
232258
=== 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-10-03 00:52:37 +0000
@@ -29,6 +29,7 @@
29 AfterPreprocessing,29 AfterPreprocessing,
30 Equals,30 Equals,
31 Is,31 Is,
32 MatchesListwise,
32 MatchesSetwise,33 MatchesSetwise,
33 MatchesStructure,34 MatchesStructure,
34 )35 )
@@ -56,6 +57,8 @@
56from lp.services.config import config57from lp.services.config import config
57from lp.services.database.constants import UTC_NOW58from lp.services.database.constants import UTC_NOW
58from lp.services.features.testing import FeatureFixture59from lp.services.features.testing import FeatureFixture
60from lp.services.job.interfaces.job import JobStatus
61from lp.services.propertycache import get_property_cache
59from lp.services.webapp import canonical_url62from lp.services.webapp import canonical_url
60from lp.services.webapp.servers import LaunchpadTestRequest63from lp.services.webapp.servers import LaunchpadTestRequest
61from lp.snappy.browser.snap import (64from lp.snappy.browser.snap import (
@@ -1344,7 +1347,7 @@
1344 "This snap package has not been built yet.",1347 "This snap package has not been built yet.",
1345 self.getMainText(snap))1348 self.getMainText(snap))
13461349
1347 def test_index_pending(self):1350 def test_index_pending_build(self):
1348 # A pending build is listed as such.1351 # A pending build is listed as such.
1349 build = self.makeBuild()1352 build = self.makeBuild()
1350 build.queueBuild()1353 build.queueBuild()
@@ -1355,6 +1358,38 @@
1355 Primary Archive for Ubuntu Linux1358 Primary Archive for Ubuntu Linux
1356 """, self.getMainText(build.snap))1359 """, self.getMainText(build.snap))
13571360
1361 def test_index_pending_build_request(self):
1362 # A pending build request is listed as such.
1363 snap = self.makeSnap()
1364 with person_logged_in(snap.owner):
1365 snap.requestBuilds(
1366 snap.owner, snap.distro_series.main_archive,
1367 PackagePublishingPocket.UPDATES)
1368 self.assertTextMatchesExpressionIgnoreWhitespace("""\
1369 Latest builds
1370 Status When complete Architecture Archive
1371 Pending build request
1372 Primary Archive for Ubuntu Linux
1373 """, self.getMainText(snap))
1374
1375 def test_index_failed_build_request(self):
1376 # A failed build request is listed as such, with its error message.
1377 snap = self.makeSnap()
1378 with person_logged_in(snap.owner):
1379 request = snap.requestBuilds(
1380 snap.owner, snap.distro_series.main_archive,
1381 PackagePublishingPocket.UPDATES)
1382 job = removeSecurityProxy(removeSecurityProxy(request)._job)
1383 job.job._status = JobStatus.FAILED
1384 job.job.date_finished = datetime.now(pytz.UTC) - timedelta(hours=1)
1385 job.error_message = "Boom"
1386 self.assertTextMatchesExpressionIgnoreWhitespace("""\
1387 Latest builds
1388 Status When complete Architecture Archive
1389 Failed build request 1 hour ago \(Boom\)
1390 Primary Archive for Ubuntu Linux
1391 """, self.getMainText(snap))
1392
1358 def test_index_store_upload(self):1393 def test_index_store_upload(self):
1359 # If the snap package is to be automatically uploaded to the store,1394 # If the snap package is to be automatically uploaded to the store,
1360 # the index page shows details of this.1395 # the index page shows details of this.
@@ -1382,8 +1417,8 @@
1382 build.updateStatus(1417 build.updateStatus(
1383 status, date_finished=build.date_started + timedelta(minutes=30))1418 status, date_finished=build.date_started + timedelta(minutes=30))
13841419
1385 def test_builds(self):1420 def test_builds_and_requests(self):
1386 # SnapView.builds produces reasonable results.1421 # SnapView.builds_and_requests produces reasonable results.
1387 snap = self.makeSnap()1422 snap = self.makeSnap()
1388 # Create oldest builds first so that they sort properly by id.1423 # Create oldest builds first so that they sort properly by id.
1389 date_gen = time_counter(1424 date_gen = time_counter(
@@ -1392,16 +1427,67 @@
1392 self.makeBuild(snap=snap, date_created=next(date_gen))1427 self.makeBuild(snap=snap, date_created=next(date_gen))
1393 for i in range(11)]1428 for i in range(11)]
1394 view = SnapView(snap, None)1429 view = SnapView(snap, None)
1395 self.assertEqual(list(reversed(builds)), view.builds)1430 self.assertEqual(list(reversed(builds)), view.builds_and_requests)
1396 self.setStatus(builds[10], BuildStatus.FULLYBUILT)1431 self.setStatus(builds[10], BuildStatus.FULLYBUILT)
1397 self.setStatus(builds[9], BuildStatus.FAILEDTOBUILD)1432 self.setStatus(builds[9], BuildStatus.FAILEDTOBUILD)
1433 del get_property_cache(view).builds_and_requests
1398 # When there are >= 9 pending builds, only the most recent of any1434 # When there are >= 9 pending builds, only the most recent of any
1399 # completed builds is returned.1435 # completed builds is returned.
1400 self.assertEqual(1436 self.assertEqual(
1401 list(reversed(builds[:9])) + [builds[10]], view.builds)1437 list(reversed(builds[:9])) + [builds[10]],
1438 view.builds_and_requests)
1402 for build in builds[:9]:1439 for build in builds[:9]:
1403 self.setStatus(build, BuildStatus.FULLYBUILT)1440 self.setStatus(build, BuildStatus.FULLYBUILT)
1404 self.assertEqual(list(reversed(builds[1:])), view.builds)1441 del get_property_cache(view).builds_and_requests
1442 self.assertEqual(list(reversed(builds[1:])), view.builds_and_requests)
1443
1444 def test_builds_and_requests_shows_build_requests(self):
1445 # SnapView.builds_and_requests interleaves build requests with
1446 # builds.
1447 snap = self.makeSnap()
1448 date_gen = time_counter(
1449 datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1))
1450 builds = [
1451 self.makeBuild(snap=snap, date_created=next(date_gen))
1452 for i in range(3)]
1453 self.setStatus(builds[2], BuildStatus.FULLYBUILT)
1454 with person_logged_in(snap.owner):
1455 request = snap.requestBuilds(
1456 snap.owner, snap.distro_series.main_archive,
1457 PackagePublishingPocket.UPDATES)
1458 job = removeSecurityProxy(removeSecurityProxy(request)._job)
1459 job.job.date_created = next(date_gen)
1460 view = SnapView(snap, None)
1461 # The pending build request is interleaved in date order with
1462 # pending builds, and these are followed by completed builds.
1463 self.assertThat(view.builds_and_requests, MatchesListwise([
1464 MatchesStructure.byEquality(id=request.id),
1465 Equals(builds[1]),
1466 Equals(builds[0]),
1467 Equals(builds[2]),
1468 ]))
1469 transaction.commit()
1470 builds.append(self.makeBuild(snap=snap))
1471 del get_property_cache(view).builds_and_requests
1472 self.assertThat(view.builds_and_requests, MatchesListwise([
1473 Equals(builds[3]),
1474 MatchesStructure.byEquality(id=request.id),
1475 Equals(builds[1]),
1476 Equals(builds[0]),
1477 Equals(builds[2]),
1478 ]))
1479 # If we pretend that the job failed, it is still listed, but after
1480 # any pending builds.
1481 job.job._status = JobStatus.FAILED
1482 job.job.date_finished = job.date_created + timedelta(minutes=30)
1483 del get_property_cache(view).builds_and_requests
1484 self.assertThat(view.builds_and_requests, MatchesListwise([
1485 Equals(builds[3]),
1486 Equals(builds[1]),
1487 Equals(builds[0]),
1488 MatchesStructure.byEquality(id=request.id),
1489 Equals(builds[2]),
1490 ]))
14051491
1406 def test_store_channels_empty(self):1492 def test_store_channels_empty(self):
1407 snap = self.factory.makeSnap()1493 snap = self.factory.makeSnap()
14081494
=== 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-10-03 00:52:37 +0000
@@ -459,6 +459,11 @@
459 value_type=Reference(ISnapBuildRequest),459 value_type=Reference(ISnapBuildRequest),
460 required=True, readonly=True)))460 required=True, readonly=True)))
461461
462 failed_build_requests = exported(doNotSnapshot(CollectionField(
463 title=_("Failed build requests for this snap package."),
464 value_type=Reference(ISnapBuildRequest),
465 required=True, readonly=True)))
466
462 # XXX cjwatson 2018-06-20: Deprecated as an exported method; can become467 # XXX cjwatson 2018-06-20: Deprecated as an exported method; can become
463 # an internal helper method once production JavaScript no longer uses468 # an internal helper method once production JavaScript no longer uses
464 # it.469 # it.
465470
=== 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-10-03 00:52:37 +0000
@@ -674,7 +674,17 @@
674 # The returned jobs are ordered by descending ID.674 # The returned jobs are ordered by descending ID.
675 jobs = job_source.findBySnap(675 jobs = job_source.findBySnap(
676 self, statuses=(JobStatus.WAITING, JobStatus.RUNNING))676 self, statuses=(JobStatus.WAITING, JobStatus.RUNNING))
677 return [SnapBuildRequest.fromJob(job) for job in jobs]677 return DecoratedResultSet(
678 jobs, result_decorator=SnapBuildRequest.fromJob)
679
680 @property
681 def failed_build_requests(self):
682 """See `ISnap`."""
683 job_source = getUtility(ISnapRequestBuildsJobSource)
684 # The returned jobs are ordered by descending ID.
685 jobs = job_source.findBySnap(self, statuses=(JobStatus.FAILED,))
686 return DecoratedResultSet(
687 jobs, result_decorator=SnapBuildRequest.fromJob)
678688
679 def _getBuilds(self, filter_term, order_by):689 def _getBuilds(self, filter_term, order_by):
680 """The actual query to get the builds."""690 """The actual query to get the builds."""
681691
=== 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-10-03 00:52:37 +0000
@@ -156,51 +156,62 @@
156 </tr>156 </tr>
157 </thead>157 </thead>
158 <tbody>158 <tbody>
159 <tal:snap-build-requests repeat="request context/pending_build_requests">159 <tal:snap-builds-and-requests repeat="item view/builds_and_requests">
160 <tr tal:attributes="id string:request-${request/id}">160 <tal:snap-build-request condition="item/date_requested|nothing">
161 <td colspan="3"161 <tr tal:define="request item"
162 tal:attributes="class string:request_status ${request/status/name}">162 tal:attributes="id string:request-${request/id}">
163 <span tal:replace="structure request/image:icon"/>163 <td tal:attributes="class string:request_status ${request/status/name}">
164 <tal:title replace="request/status/title"/> build request164 <span tal:replace="structure request/image:icon"/>
165 </td>165 <tal:title replace="request/status/title"/> build request
166 <td>166 </td>
167 <tal:archive replace="structure request/archive/fmt:link"/>167 <td>
168 </td>168 <tal:date condition="request/date_finished"
169 </tr>169 replace="request/date_finished/fmt:displaydate"/>
170 </tal:snap-build-requests>170 <tal:error-message condition="request/error_message">
171 <tal:snap-builds repeat="build view/builds">171 (<span tal:replace="request/error_message"/>)
172 <tr tal:attributes="id string:build-${build/id}">172 </tal:error-message>
173 <td tal:attributes="class string:build_status ${build/status/name}">173 </td>
174 <span tal:replace="structure build/image:icon"/>174 <td/>
175 <a tal:content="build/status/title"175 <td>
176 tal:attributes="href build/fmt:url"/>176 <tal:archive replace="structure request/archive/fmt:link"/>
177 </td>177 </td>
178 <td class="datebuilt">178 </tr>
179 <tal:date replace="build/date/fmt:displaydate"/>179 </tal:snap-build-request>
180 <tal:estimate condition="build/estimate">180 <tal:snap-build condition="not: item/date_requested|nothing">
181 (estimated)181 <tr tal:define="build item"
182 </tal:estimate>182 tal:attributes="id string:build-${build/id}">
183 <td tal:attributes="class string:build_status ${build/status/name}">
184 <span tal:replace="structure build/image:icon"/>
185 <a tal:content="build/status/title"
186 tal:attributes="href build/fmt:url"/>
187 </td>
188 <td class="datebuilt">
189 <tal:date replace="build/date/fmt:displaydate"/>
190 <tal:estimate condition="build/estimate">
191 (estimated)
192 </tal:estimate>
183193
184 <tal:build-log define="file build/log" tal:condition="file">194 <tal:build-log define="file build/log" tal:condition="file">
185 <a class="sprite download"195 <a class="sprite download"
186 tal:attributes="href build/log_url">buildlog</a>196 tal:attributes="href build/log_url">buildlog</a>
187 (<span tal:replace="file/content/filesize/fmt:bytes"/>)197 (<span tal:replace="file/content/filesize/fmt:bytes"/>)
188 </tal:build-log>198 </tal:build-log>
189 </td>199 </td>
190 <td>200 <td>
191 <a class="sprite distribution"201 <a class="sprite distribution"
192 tal:define="archseries build/distro_arch_series"202 tal:define="archseries build/distro_arch_series"
193 tal:attributes="href archseries/fmt:url"203 tal:attributes="href archseries/fmt:url"
194 tal:content="archseries/architecturetag"/>204 tal:content="archseries/architecturetag"/>
195 </td>205 </td>
196 <td>206 <td>
197 <tal:archive replace="structure build/archive/fmt:link"/>207 <tal:archive replace="structure build/archive/fmt:link"/>
198 </td>208 </td>
199 </tr>209 </tr>
200 </tal:snap-builds>210 </tal:snap-build>
211 </tal:snap-builds-and-requests>
201 </tbody>212 </tbody>
202 </table>213 </table>
203 <p tal:condition="not: view/builds">214 <p tal:condition="not: view/builds_and_requests">
204 This snap package has not been built yet.215 This snap package has not been built yet.
205 </p>216 </p>
206 <div tal:define="link context/menu:context/request_builds"217 <div tal:define="link context/menu:context/request_builds"
207218
=== 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-10-03 00:52:37 +0000
@@ -177,7 +177,7 @@
177 self.assertThat(177 self.assertThat(
178 self.factory.makeSnap(),178 self.factory.makeSnap(),
179 DoesNotSnapshot(179 DoesNotSnapshot(
180 ["pending_build_requests",180 ["pending_build_requests", "failed_build_requests",
181 "builds", "completed_builds", "pending_builds"],181 "builds", "completed_builds", "pending_builds"],
182 ISnapView))182 ISnapView))
183183