Merge lp:~cjwatson/launchpad/snap-failed-build-requests into lp:launchpad
- snap-failed-build-requests
- Merge into devel
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 | ||||
Related bugs: |
|
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_
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
1 | === modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py' | |||
2 | --- lib/lp/code/browser/tests/test_branchmergeproposal.py 2018-06-21 17:26:43 +0000 | |||
3 | +++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2018-10-03 00:52:37 +0000 | |||
4 | @@ -93,6 +93,7 @@ | |||
5 | 93 | from lp.services.librarian.interfaces.client import LibrarianServerError | 93 | from lp.services.librarian.interfaces.client import LibrarianServerError |
6 | 94 | from lp.services.messages.model.message import MessageSet | 94 | from lp.services.messages.model.message import MessageSet |
7 | 95 | from lp.services.timeout import TimeoutError | 95 | from lp.services.timeout import TimeoutError |
8 | 96 | from lp.services.utils import seconds_since_epoch | ||
9 | 96 | from lp.services.webapp import canonical_url | 97 | from lp.services.webapp import canonical_url |
10 | 97 | from lp.services.webapp.interfaces import BrowserNotificationLevel | 98 | from lp.services.webapp.interfaces import BrowserNotificationLevel |
11 | 98 | from lp.services.webapp.servers import LaunchpadTestRequest | 99 | from lp.services.webapp.servers import LaunchpadTestRequest |
12 | @@ -1515,7 +1516,6 @@ | |||
13 | 1515 | author = self.factory.makePerson() | 1516 | author = self.factory.makePerson() |
14 | 1516 | with person_logged_in(author): | 1517 | with person_logged_in(author): |
15 | 1517 | author_email = author.preferredemail.email | 1518 | author_email = author.preferredemail.email |
16 | 1518 | epoch = datetime.fromtimestamp(0, tz=pytz.UTC) | ||
17 | 1519 | review_date = self.factory.getUniqueDate() | 1519 | review_date = self.factory.getUniqueDate() |
18 | 1520 | commit_date = self.factory.getUniqueDate() | 1520 | commit_date = self.factory.getUniqueDate() |
19 | 1521 | bmp = self.factory.makeBranchMergeProposalForGit( | 1521 | bmp = self.factory.makeBranchMergeProposalForGit( |
20 | @@ -1527,7 +1527,7 @@ | |||
21 | 1527 | 'author': { | 1527 | 'author': { |
22 | 1528 | 'name': author.display_name, | 1528 | 'name': author.display_name, |
23 | 1529 | 'email': author_email, | 1529 | 'email': author_email, |
25 | 1530 | 'time': int((commit_date - epoch).total_seconds()), | 1530 | 'time': int(seconds_since_epoch(commit_date)), |
26 | 1531 | }, | 1531 | }, |
27 | 1532 | } | 1532 | } |
28 | 1533 | ])) | 1533 | ])) |
29 | @@ -1614,7 +1614,6 @@ | |||
30 | 1614 | # SHA-1 and can ask the repository for its unmerged commits. | 1614 | # SHA-1 and can ask the repository for its unmerged commits. |
31 | 1615 | bmp = self.factory.makeBranchMergeProposalForGit() | 1615 | bmp = self.factory.makeBranchMergeProposalForGit() |
32 | 1616 | sha1 = unicode(hashlib.sha1(b'0').hexdigest()) | 1616 | sha1 = unicode(hashlib.sha1(b'0').hexdigest()) |
33 | 1617 | epoch = datetime.fromtimestamp(0, tz=pytz.UTC) | ||
34 | 1618 | commit_date = datetime(2015, 1, 1, tzinfo=pytz.UTC) | 1617 | commit_date = datetime(2015, 1, 1, tzinfo=pytz.UTC) |
35 | 1619 | self.useFixture(GitHostingFixture(log=[ | 1618 | self.useFixture(GitHostingFixture(log=[ |
36 | 1620 | { | 1619 | { |
37 | @@ -1623,7 +1622,7 @@ | |||
38 | 1623 | 'author': { | 1622 | 'author': { |
39 | 1624 | 'name': 'Example Person', | 1623 | 'name': 'Example Person', |
40 | 1625 | 'email': 'person@example.org', | 1624 | 'email': 'person@example.org', |
42 | 1626 | 'time': int((commit_date - epoch).total_seconds()), | 1625 | 'time': int(seconds_since_epoch(commit_date)), |
43 | 1627 | }, | 1626 | }, |
44 | 1628 | } | 1627 | } |
45 | 1629 | ])) | 1628 | ])) |
46 | 1630 | 1629 | ||
47 | === modified file 'lib/lp/code/browser/tests/test_gitref.py' | |||
48 | --- lib/lp/code/browser/tests/test_gitref.py 2018-09-07 13:43:50 +0000 | |||
49 | +++ lib/lp/code/browser/tests/test_gitref.py 2018-10-03 00:52:37 +0000 | |||
50 | @@ -23,6 +23,7 @@ | |||
51 | 23 | from lp.code.tests.helpers import GitHostingFixture | 23 | from lp.code.tests.helpers import GitHostingFixture |
52 | 24 | from lp.services.beautifulsoup import BeautifulSoup | 24 | from lp.services.beautifulsoup import BeautifulSoup |
53 | 25 | from lp.services.job.runner import JobRunner | 25 | from lp.services.job.runner import JobRunner |
54 | 26 | from lp.services.utils import seconds_since_epoch | ||
55 | 26 | from lp.services.webapp.publisher import canonical_url | 27 | from lp.services.webapp.publisher import canonical_url |
56 | 27 | from lp.testing import ( | 28 | from lp.testing import ( |
57 | 28 | admin_logged_in, | 29 | admin_logged_in, |
58 | @@ -145,7 +146,6 @@ | |||
59 | 145 | authors = [self.factory.makePerson() for _ in range(5)] | 146 | authors = [self.factory.makePerson() for _ in range(5)] |
60 | 146 | with admin_logged_in(): | 147 | with admin_logged_in(): |
61 | 147 | author_emails = [author.preferredemail.email for author in authors] | 148 | author_emails = [author.preferredemail.email for author in authors] |
62 | 148 | epoch = datetime.fromtimestamp(0, tz=pytz.UTC) | ||
63 | 149 | dates = [ | 149 | dates = [ |
64 | 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)] |
65 | 151 | return [ | 151 | return [ |
66 | @@ -155,12 +155,12 @@ | |||
67 | 155 | "author": { | 155 | "author": { |
68 | 156 | "name": authors[i].display_name, | 156 | "name": authors[i].display_name, |
69 | 157 | "email": author_emails[i], | 157 | "email": author_emails[i], |
71 | 158 | "time": int((dates[i] - epoch).total_seconds()), | 158 | "time": int(seconds_since_epoch(dates[i])), |
72 | 159 | }, | 159 | }, |
73 | 160 | "committer": { | 160 | "committer": { |
74 | 161 | "name": authors[i].display_name, | 161 | "name": authors[i].display_name, |
75 | 162 | "email": author_emails[i], | 162 | "email": author_emails[i], |
77 | 163 | "time": int((dates[i] - epoch).total_seconds()), | 163 | "time": int(seconds_since_epoch(dates[i])), |
78 | 164 | }, | 164 | }, |
79 | 165 | "parents": [unicode(hashlib.sha1(str(i - 1)).hexdigest())], | 165 | "parents": [unicode(hashlib.sha1(str(i - 1)).hexdigest())], |
80 | 166 | "tree": unicode(hashlib.sha1("").hexdigest()), | 166 | "tree": unicode(hashlib.sha1("").hexdigest()), |
81 | 167 | 167 | ||
82 | === modified file 'lib/lp/code/model/gitref.py' | |||
83 | --- lib/lp/code/model/gitref.py 2018-08-23 12:34:24 +0000 | |||
84 | +++ lib/lp/code/model/gitref.py 2018-10-03 00:52:37 +0000 | |||
85 | @@ -9,7 +9,6 @@ | |||
86 | 9 | 'GitRefRemote', | 9 | 'GitRefRemote', |
87 | 10 | ] | 10 | ] |
88 | 11 | 11 | ||
89 | 12 | from datetime import datetime | ||
90 | 13 | from functools import partial | 12 | from functools import partial |
91 | 14 | import json | 13 | import json |
92 | 15 | import re | 14 | import re |
93 | @@ -79,6 +78,7 @@ | |||
94 | 79 | from lp.services.features import getFeatureFlag | 78 | from lp.services.features import getFeatureFlag |
95 | 80 | from lp.services.memcache.interfaces import IMemcacheClient | 79 | from lp.services.memcache.interfaces import IMemcacheClient |
96 | 81 | from lp.services.timeout import urlfetch | 80 | from lp.services.timeout import urlfetch |
97 | 81 | from lp.services.utils import seconds_since_epoch | ||
98 | 82 | from lp.services.webapp.interfaces import ILaunchBag | 82 | from lp.services.webapp.interfaces import ILaunchBag |
99 | 83 | 83 | ||
100 | 84 | 84 | ||
101 | @@ -345,19 +345,18 @@ | |||
102 | 345 | else: | 345 | else: |
103 | 346 | # Fall back to synthesising something reasonable based on | 346 | # Fall back to synthesising something reasonable based on |
104 | 347 | # information in our own database. | 347 | # information in our own database. |
105 | 348 | epoch = datetime.fromtimestamp(0, tz=pytz.UTC) | ||
106 | 349 | log = [{ | 348 | log = [{ |
107 | 350 | "sha1": self.commit_sha1, | 349 | "sha1": self.commit_sha1, |
108 | 351 | "message": self.commit_message, | 350 | "message": self.commit_message, |
109 | 352 | "author": None if self.author is None else { | 351 | "author": None if self.author is None else { |
110 | 353 | "name": self.author.name_without_email, | 352 | "name": self.author.name_without_email, |
111 | 354 | "email": self.author.email, | 353 | "email": self.author.email, |
113 | 355 | "time": (self.author_date - epoch).total_seconds(), | 354 | "time": seconds_since_epoch(self.author_date), |
114 | 356 | }, | 355 | }, |
115 | 357 | "committer": None if self.committer is None else { | 356 | "committer": None if self.committer is None else { |
116 | 358 | "name": self.committer.name_without_email, | 357 | "name": self.committer.name_without_email, |
117 | 359 | "email": self.committer.email, | 358 | "email": self.committer.email, |
119 | 360 | "time": (self.committer_date - epoch).total_seconds(), | 359 | "time": seconds_since_epoch(self.committer_date), |
120 | 361 | }, | 360 | }, |
121 | 362 | }] | 361 | }] |
122 | 363 | return log | 362 | return log |
123 | 364 | 363 | ||
124 | === modified file 'lib/lp/code/model/tests/test_gitjob.py' | |||
125 | --- lib/lp/code/model/tests/test_gitjob.py 2017-10-04 01:29:35 +0000 | |||
126 | +++ lib/lp/code/model/tests/test_gitjob.py 2018-10-03 00:52:37 +0000 | |||
127 | @@ -1,4 +1,4 @@ | |||
129 | 1 | # Copyright 2015-2017 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
130 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
131 | 3 | 3 | ||
132 | 4 | """Tests for `GitJob`s.""" | 4 | """Tests for `GitJob`s.""" |
133 | @@ -44,6 +44,7 @@ | |||
134 | 44 | from lp.services.database.constants import UTC_NOW | 44 | from lp.services.database.constants import UTC_NOW |
135 | 45 | from lp.services.features.testing import FeatureFixture | 45 | from lp.services.features.testing import FeatureFixture |
136 | 46 | from lp.services.job.runner import JobRunner | 46 | from lp.services.job.runner import JobRunner |
137 | 47 | from lp.services.utils import seconds_since_epoch | ||
138 | 47 | from lp.services.webapp import canonical_url | 48 | from lp.services.webapp import canonical_url |
139 | 48 | from lp.testing import ( | 49 | from lp.testing import ( |
140 | 49 | TestCaseWithFactory, | 50 | TestCaseWithFactory, |
141 | @@ -97,7 +98,6 @@ | |||
142 | 97 | 98 | ||
143 | 98 | @staticmethod | 99 | @staticmethod |
144 | 99 | def makeFakeCommits(author, author_date_gen, paths): | 100 | def makeFakeCommits(author, author_date_gen, paths): |
145 | 100 | epoch = datetime.fromtimestamp(0, tz=pytz.UTC) | ||
146 | 101 | dates = {path: next(author_date_gen) for path in paths} | 101 | dates = {path: next(author_date_gen) for path in paths} |
147 | 102 | return [{ | 102 | return [{ |
148 | 103 | "sha1": unicode(hashlib.sha1(path).hexdigest()), | 103 | "sha1": unicode(hashlib.sha1(path).hexdigest()), |
149 | @@ -105,12 +105,12 @@ | |||
150 | 105 | "author": { | 105 | "author": { |
151 | 106 | "name": author.displayname, | 106 | "name": author.displayname, |
152 | 107 | "email": author.preferredemail.email, | 107 | "email": author.preferredemail.email, |
154 | 108 | "time": int((dates[path] - epoch).total_seconds()), | 108 | "time": int(seconds_since_epoch(dates[path])), |
155 | 109 | }, | 109 | }, |
156 | 110 | "committer": { | 110 | "committer": { |
157 | 111 | "name": author.displayname, | 111 | "name": author.displayname, |
158 | 112 | "email": author.preferredemail.email, | 112 | "email": author.preferredemail.email, |
160 | 113 | "time": int((dates[path] - epoch).total_seconds()), | 113 | "time": int(seconds_since_epoch(dates[path])), |
161 | 114 | }, | 114 | }, |
162 | 115 | "parents": [], | 115 | "parents": [], |
163 | 116 | "tree": unicode(hashlib.sha1("").hexdigest()), | 116 | "tree": unicode(hashlib.sha1("").hexdigest()), |
164 | 117 | 117 | ||
165 | === modified file 'lib/lp/code/model/tests/test_gitref.py' | |||
166 | --- lib/lp/code/model/tests/test_gitref.py 2018-08-23 12:34:24 +0000 | |||
167 | +++ lib/lp/code/model/tests/test_gitref.py 2018-10-03 00:52:37 +0000 | |||
168 | @@ -42,6 +42,7 @@ | |||
169 | 42 | from lp.services.config import config | 42 | from lp.services.config import config |
170 | 43 | from lp.services.features.testing import FeatureFixture | 43 | from lp.services.features.testing import FeatureFixture |
171 | 44 | from lp.services.memcache.interfaces import IMemcacheClient | 44 | from lp.services.memcache.interfaces import IMemcacheClient |
172 | 45 | from lp.services.utils import seconds_since_epoch | ||
173 | 45 | from lp.services.webapp.interfaces import OAuthPermission | 46 | from lp.services.webapp.interfaces import OAuthPermission |
174 | 46 | from lp.testing import ( | 47 | from lp.testing import ( |
175 | 47 | admin_logged_in, | 48 | admin_logged_in, |
176 | @@ -134,7 +135,6 @@ | |||
177 | 134 | with admin_logged_in(): | 135 | with admin_logged_in(): |
178 | 135 | self.author_emails = [ | 136 | self.author_emails = [ |
179 | 136 | author.preferredemail.email for author in self.authors] | 137 | author.preferredemail.email for author in self.authors] |
180 | 137 | epoch = datetime.fromtimestamp(0, tz=pytz.UTC) | ||
181 | 138 | self.dates = [ | 138 | self.dates = [ |
182 | 139 | datetime(2015, 1, 1, 0, 0, 0, tzinfo=pytz.UTC), | 139 | datetime(2015, 1, 1, 0, 0, 0, tzinfo=pytz.UTC), |
183 | 140 | datetime(2015, 1, 2, 0, 0, 0, tzinfo=pytz.UTC), | 140 | datetime(2015, 1, 2, 0, 0, 0, tzinfo=pytz.UTC), |
184 | @@ -148,12 +148,12 @@ | |||
185 | 148 | "author": { | 148 | "author": { |
186 | 149 | "name": self.authors[0].display_name, | 149 | "name": self.authors[0].display_name, |
187 | 150 | "email": self.author_emails[0], | 150 | "email": self.author_emails[0], |
189 | 151 | "time": int((self.dates[1] - epoch).total_seconds()), | 151 | "time": int(seconds_since_epoch(self.dates[1])), |
190 | 152 | }, | 152 | }, |
191 | 153 | "committer": { | 153 | "committer": { |
192 | 154 | "name": self.authors[1].display_name, | 154 | "name": self.authors[1].display_name, |
193 | 155 | "email": self.author_emails[1], | 155 | "email": self.author_emails[1], |
195 | 156 | "time": int((self.dates[1] - epoch).total_seconds()), | 156 | "time": int(seconds_since_epoch(self.dates[1])), |
196 | 157 | }, | 157 | }, |
197 | 158 | "parents": [self.sha1_root], | 158 | "parents": [self.sha1_root], |
198 | 159 | "tree": unicode(hashlib.sha1("").hexdigest()), | 159 | "tree": unicode(hashlib.sha1("").hexdigest()), |
199 | @@ -164,12 +164,12 @@ | |||
200 | 164 | "author": { | 164 | "author": { |
201 | 165 | "name": self.authors[1].display_name, | 165 | "name": self.authors[1].display_name, |
202 | 166 | "email": self.author_emails[1], | 166 | "email": self.author_emails[1], |
204 | 167 | "time": int((self.dates[0] - epoch).total_seconds()), | 167 | "time": int(seconds_since_epoch(self.dates[0])), |
205 | 168 | }, | 168 | }, |
206 | 169 | "committer": { | 169 | "committer": { |
207 | 170 | "name": self.authors[0].display_name, | 170 | "name": self.authors[0].display_name, |
208 | 171 | "email": self.author_emails[0], | 171 | "email": self.author_emails[0], |
210 | 172 | "time": int((self.dates[0] - epoch).total_seconds()), | 172 | "time": int(seconds_since_epoch(self.dates[0])), |
211 | 173 | }, | 173 | }, |
212 | 174 | "parents": [], | 174 | "parents": [], |
213 | 175 | "tree": unicode(hashlib.sha1("").hexdigest()), | 175 | "tree": unicode(hashlib.sha1("").hexdigest()), |
214 | 176 | 176 | ||
215 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' | |||
216 | --- lib/lp/code/model/tests/test_gitrepository.py 2018-08-31 14:25:40 +0000 | |||
217 | +++ lib/lp/code/model/tests/test_gitrepository.py 2018-10-03 00:52:37 +0000 | |||
218 | @@ -1,4 +1,4 @@ | |||
220 | 1 | # Copyright 2015-2017 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
221 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
222 | 3 | 3 | ||
223 | 4 | """Tests for Git repositories.""" | 4 | """Tests for Git repositories.""" |
224 | @@ -124,6 +124,7 @@ | |||
225 | 124 | from lp.services.job.runner import JobRunner | 124 | from lp.services.job.runner import JobRunner |
226 | 125 | from lp.services.mail import stub | 125 | from lp.services.mail import stub |
227 | 126 | from lp.services.propertycache import clear_property_cache | 126 | from lp.services.propertycache import clear_property_cache |
228 | 127 | from lp.services.utils import seconds_since_epoch | ||
229 | 127 | from lp.services.webapp.authorization import check_permission | 128 | from lp.services.webapp.authorization import check_permission |
230 | 128 | from lp.services.webapp.interfaces import OAuthPermission | 129 | from lp.services.webapp.interfaces import OAuthPermission |
231 | 129 | from lp.testing import ( | 130 | from lp.testing import ( |
232 | @@ -1283,7 +1284,6 @@ | |||
233 | 1283 | author = self.factory.makePerson() | 1284 | author = self.factory.makePerson() |
234 | 1284 | with person_logged_in(author): | 1285 | with person_logged_in(author): |
235 | 1285 | author_email = author.preferredemail.email | 1286 | author_email = author.preferredemail.email |
236 | 1286 | epoch = datetime.fromtimestamp(0, tz=pytz.UTC) | ||
237 | 1287 | author_date = datetime(2015, 1, 1, tzinfo=pytz.UTC) | 1287 | author_date = datetime(2015, 1, 1, tzinfo=pytz.UTC) |
238 | 1288 | committer_date = datetime(2015, 1, 2, tzinfo=pytz.UTC) | 1288 | committer_date = datetime(2015, 1, 2, tzinfo=pytz.UTC) |
239 | 1289 | hosting_fixture = self.useFixture(GitHostingFixture(commits=[ | 1289 | hosting_fixture = self.useFixture(GitHostingFixture(commits=[ |
240 | @@ -1293,12 +1293,12 @@ | |||
241 | 1293 | "author": { | 1293 | "author": { |
242 | 1294 | "name": author.displayname, | 1294 | "name": author.displayname, |
243 | 1295 | "email": author_email, | 1295 | "email": author_email, |
245 | 1296 | "time": int((author_date - epoch).total_seconds()), | 1296 | "time": int(seconds_since_epoch(author_date)), |
246 | 1297 | }, | 1297 | }, |
247 | 1298 | "committer": { | 1298 | "committer": { |
248 | 1299 | "name": "New Person", | 1299 | "name": "New Person", |
249 | 1300 | "email": "new-person@example.org", | 1300 | "email": "new-person@example.org", |
251 | 1301 | "time": int((committer_date - epoch).total_seconds()), | 1301 | "time": int(seconds_since_epoch(committer_date)), |
252 | 1302 | }, | 1302 | }, |
253 | 1303 | "parents": [], | 1303 | "parents": [], |
254 | 1304 | "tree": unicode(hashlib.sha1("").hexdigest()), | 1304 | "tree": unicode(hashlib.sha1("").hexdigest()), |
255 | 1305 | 1305 | ||
256 | === modified file 'lib/lp/code/stories/branches/xx-code-review-comments.txt' | |||
257 | --- lib/lp/code/stories/branches/xx-code-review-comments.txt 2018-05-13 10:35:52 +0000 | |||
258 | +++ lib/lp/code/stories/branches/xx-code-review-comments.txt 2018-10-03 00:52:37 +0000 | |||
259 | @@ -192,11 +192,11 @@ | |||
260 | 192 | log entries first. | 192 | log entries first. |
261 | 193 | 193 | ||
262 | 194 | >>> from lp.code.tests.helpers import GitHostingFixture | 194 | >>> from lp.code.tests.helpers import GitHostingFixture |
263 | 195 | >>> from lp.services.utils import seconds_since_epoch | ||
264 | 195 | 196 | ||
265 | 196 | >>> login('admin@canonical.com') | 197 | >>> login('admin@canonical.com') |
266 | 197 | >>> bmp = factory.makeBranchMergeProposalForGit() | 198 | >>> bmp = factory.makeBranchMergeProposalForGit() |
267 | 198 | >>> bmp.requestReview(review_date) | 199 | >>> bmp.requestReview(review_date) |
268 | 199 | >>> epoch = datetime.fromtimestamp(0, tz=pytz.UTC) | ||
269 | 200 | >>> commit_date = review_date + timedelta(days=1) | 200 | >>> commit_date = review_date + timedelta(days=1) |
270 | 201 | >>> hosting_fixture = GitHostingFixture() | 201 | >>> hosting_fixture = GitHostingFixture() |
271 | 202 | >>> for i in range(2): | 202 | >>> for i in range(2): |
272 | @@ -206,7 +206,7 @@ | |||
273 | 206 | ... u'author': { | 206 | ... u'author': { |
274 | 207 | ... u'name': bmp.registrant.display_name, | 207 | ... u'name': bmp.registrant.display_name, |
275 | 208 | ... u'email': bmp.registrant.preferredemail.email, | 208 | ... u'email': bmp.registrant.preferredemail.email, |
277 | 209 | ... u'time': int((commit_date - epoch).total_seconds()), | 209 | ... u'time': int(seconds_since_epoch(commit_date)), |
278 | 210 | ... }, | 210 | ... }, |
279 | 211 | ... }) | 211 | ... }) |
280 | 212 | ... hosting_fixture.getLog.result.insert(0, { | 212 | ... hosting_fixture.getLog.result.insert(0, { |
281 | @@ -215,7 +215,7 @@ | |||
282 | 215 | ... u'author': { | 215 | ... u'author': { |
283 | 216 | ... u'name': bmp.registrant.display_name, | 216 | ... u'name': bmp.registrant.display_name, |
284 | 217 | ... u'email': bmp.registrant.preferredemail.email, | 217 | ... u'email': bmp.registrant.preferredemail.email, |
286 | 218 | ... u'time': int((commit_date - epoch).total_seconds()), | 218 | ... u'time': int(seconds_since_epoch(commit_date)), |
287 | 219 | ... }, | 219 | ... }, |
288 | 220 | ... }) | 220 | ... }) |
289 | 221 | ... commit_date += timedelta(days=1) | 221 | ... commit_date += timedelta(days=1) |
290 | 222 | 222 | ||
291 | === modified file 'lib/lp/services/tests/test_utils.py' | |||
292 | --- lib/lp/services/tests/test_utils.py 2018-04-17 09:41:46 +0000 | |||
293 | +++ lib/lp/services/tests/test_utils.py 2018-10-03 00:52:37 +0000 | |||
294 | @@ -37,6 +37,7 @@ | |||
295 | 37 | run_capturing_output, | 37 | run_capturing_output, |
296 | 38 | sanitise_urls, | 38 | sanitise_urls, |
297 | 39 | save_bz2_pickle, | 39 | save_bz2_pickle, |
298 | 40 | seconds_since_epoch, | ||
299 | 40 | traceback_info, | 41 | traceback_info, |
300 | 41 | utc_now, | 42 | utc_now, |
301 | 42 | ) | 43 | ) |
302 | @@ -380,6 +381,18 @@ | |||
303 | 380 | self.assertThat(now, LessThanOrEqual(new_now)) | 381 | self.assertThat(now, LessThanOrEqual(new_now)) |
304 | 381 | 382 | ||
305 | 382 | 383 | ||
306 | 384 | class TestSecondsSinceEpoch(TestCase): | ||
307 | 385 | """Tests for `seconds_since_epoch`.""" | ||
308 | 386 | |||
309 | 387 | def test_epoch(self): | ||
310 | 388 | epoch = datetime.fromtimestamp(0, tz=UTC) | ||
311 | 389 | self.assertEqual(0, seconds_since_epoch(epoch)) | ||
312 | 390 | |||
313 | 391 | def test_start_of_2018(self): | ||
314 | 392 | dt = datetime(2018, 1, 1, tzinfo=UTC) | ||
315 | 393 | self.assertEqual(1514764800, seconds_since_epoch(dt)) | ||
316 | 394 | |||
317 | 395 | |||
318 | 383 | class TestBZ2Pickle(TestCase): | 396 | class TestBZ2Pickle(TestCase): |
319 | 384 | """Tests for `save_bz2_pickle` and `load_bz2_pickle`.""" | 397 | """Tests for `save_bz2_pickle` and `load_bz2_pickle`.""" |
320 | 385 | 398 | ||
321 | 386 | 399 | ||
322 | === modified file 'lib/lp/services/utils.py' | |||
323 | --- lib/lp/services/utils.py 2018-04-17 09:41:46 +0000 | |||
324 | +++ lib/lp/services/utils.py 2018-10-03 00:52:37 +0000 | |||
325 | @@ -25,6 +25,7 @@ | |||
326 | 25 | 'run_capturing_output', | 25 | 'run_capturing_output', |
327 | 26 | 'sanitise_urls', | 26 | 'sanitise_urls', |
328 | 27 | 'save_bz2_pickle', | 27 | 'save_bz2_pickle', |
329 | 28 | 'seconds_since_epoch', | ||
330 | 28 | 'text_delta', | 29 | 'text_delta', |
331 | 29 | 'traceback_info', | 30 | 'traceback_info', |
332 | 30 | 'utc_now', | 31 | 'utc_now', |
333 | @@ -302,6 +303,14 @@ | |||
334 | 302 | return datetime.now(tz=pytz.UTC) | 303 | return datetime.now(tz=pytz.UTC) |
335 | 303 | 304 | ||
336 | 304 | 305 | ||
337 | 306 | _epoch = datetime.fromtimestamp(0, tz=pytz.UTC) | ||
338 | 307 | |||
339 | 308 | |||
340 | 309 | def seconds_since_epoch(dt): | ||
341 | 310 | """Express a `datetime` as the number of seconds since the Unix epoch.""" | ||
342 | 311 | return (dt - _epoch).total_seconds() | ||
343 | 312 | |||
344 | 313 | |||
345 | 305 | # This is a regular expression that matches email address embedded in | 314 | # This is a regular expression that matches email address embedded in |
346 | 306 | # text. It is not RFC 2821 compliant, nor does it need to be. This | 315 | # text. It is not RFC 2821 compliant, nor does it need to be. This |
347 | 307 | # expression strives to identify probable email addresses so that they | 316 | # expression strives to identify probable email addresses so that they |
348 | 308 | 317 | ||
349 | === modified file 'lib/lp/snappy/browser/snap.py' | |||
350 | --- lib/lp/snappy/browser/snap.py 2018-09-13 15:21:05 +0000 | |||
351 | +++ lib/lp/snappy/browser/snap.py 2018-10-03 00:52:37 +0000 | |||
352 | @@ -57,7 +57,9 @@ | |||
353 | 57 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 57 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
354 | 58 | from lp.services.features import getFeatureFlag | 58 | from lp.services.features import getFeatureFlag |
355 | 59 | from lp.services.helpers import english_list | 59 | from lp.services.helpers import english_list |
356 | 60 | from lp.services.propertycache import cachedproperty | ||
357 | 60 | from lp.services.scripts import log | 61 | from lp.services.scripts import log |
358 | 62 | from lp.services.utils import seconds_since_epoch | ||
359 | 61 | from lp.services.webapp import ( | 63 | from lp.services.webapp import ( |
360 | 62 | canonical_url, | 64 | canonical_url, |
361 | 63 | ContextMenu, | 65 | ContextMenu, |
362 | @@ -178,9 +180,9 @@ | |||
363 | 178 | class SnapView(LaunchpadView): | 180 | class SnapView(LaunchpadView): |
364 | 179 | """Default view of a Snap.""" | 181 | """Default view of a Snap.""" |
365 | 180 | 182 | ||
369 | 181 | @property | 183 | @cachedproperty |
370 | 182 | def builds(self): | 184 | def builds_and_requests(self): |
371 | 183 | return builds_for_snap(self.context) | 185 | return builds_and_requests_for_snap(self.context) |
372 | 184 | 186 | ||
373 | 185 | @property | 187 | @property |
374 | 186 | def person_picker(self): | 188 | def person_picker(self): |
375 | @@ -209,23 +211,47 @@ | |||
376 | 209 | return ', '.join(self.context.store_channels) | 211 | return ', '.join(self.context.store_channels) |
377 | 210 | 212 | ||
378 | 211 | 213 | ||
381 | 212 | def builds_for_snap(snap): | 214 | def builds_and_requests_for_snap(snap): |
382 | 213 | """A list of interesting builds. | 215 | """A list of interesting builds and build requests. |
383 | 214 | 216 | ||
390 | 215 | All pending builds are shown, as well as 1-10 recent builds. Recent | 217 | All pending builds and pending build requests are shown, as well as up |
391 | 216 | builds are ordered by date finished (if completed) or date_started (if | 218 | to 10 recent builds and recent failed build requests. Pending items are |
392 | 217 | date finished is not set due to an error building or other circumstance | 219 | ordered by the date they were created; recent items are ordered by the |
393 | 218 | which resulted in the build not being completed). This allows started | 220 | date they finished (if available) or the date they started (if the date |
394 | 219 | but unfinished builds to show up in the view but be discarded as more | 221 | they finished is not set due to an error). This allows started but |
395 | 220 | recent builds become available. | 222 | unfinished builds to show up in the view but be discarded as more recent |
396 | 223 | builds become available. | ||
397 | 221 | 224 | ||
398 | 222 | Builds that the user does not have permission to see are excluded (by | 225 | Builds that the user does not have permission to see are excluded (by |
399 | 223 | the model code). | 226 | the model code). |
400 | 224 | """ | 227 | """ |
405 | 225 | builds = list(snap.pending_builds) | 228 | # We need to interleave items of different types, so SQL can't do all |
406 | 226 | if len(builds) < 10: | 229 | # the sorting for us. |
407 | 227 | builds.extend(snap.completed_builds[:10 - len(builds)]) | 230 | def make_sort_key(*date_attrs): |
408 | 228 | return builds | 231 | def _sort_key(item): |
409 | 232 | for date_attr in date_attrs: | ||
410 | 233 | if getattr(item, date_attr, None) is not None: | ||
411 | 234 | return -seconds_since_epoch(getattr(item, date_attr)) | ||
412 | 235 | return 0 | ||
413 | 236 | |||
414 | 237 | return _sort_key | ||
415 | 238 | |||
416 | 239 | items = sorted( | ||
417 | 240 | list(snap.pending_builds) + list(snap.pending_build_requests), | ||
418 | 241 | key=make_sort_key("date_created", "date_requested")) | ||
419 | 242 | if len(items) < 10: | ||
420 | 243 | # We need to interleave two unbounded result sets, but we only need | ||
421 | 244 | # enough items from them to make the total count up to 10. It's | ||
422 | 245 | # simplest to just fetch the upper bound from each set and do our | ||
423 | 246 | # own sorting. | ||
424 | 247 | recent_items = sorted( | ||
425 | 248 | list(snap.completed_builds[:10 - len(items)]) + | ||
426 | 249 | list(snap.failed_build_requests[:10 - len(items)]), | ||
427 | 250 | key=make_sort_key( | ||
428 | 251 | "date_finished", "date_started", | ||
429 | 252 | "date_created", "date_requested")) | ||
430 | 253 | items.extend(recent_items[:10 - len(items)]) | ||
431 | 254 | return items | ||
432 | 229 | 255 | ||
433 | 230 | 256 | ||
434 | 231 | def new_builds_notification_text(builds, already_pending=None): | 257 | def new_builds_notification_text(builds, already_pending=None): |
435 | 232 | 258 | ||
436 | === modified file 'lib/lp/snappy/browser/tests/test_snap.py' | |||
437 | --- lib/lp/snappy/browser/tests/test_snap.py 2018-09-13 15:21:05 +0000 | |||
438 | +++ lib/lp/snappy/browser/tests/test_snap.py 2018-10-03 00:52:37 +0000 | |||
439 | @@ -29,6 +29,7 @@ | |||
440 | 29 | AfterPreprocessing, | 29 | AfterPreprocessing, |
441 | 30 | Equals, | 30 | Equals, |
442 | 31 | Is, | 31 | Is, |
443 | 32 | MatchesListwise, | ||
444 | 32 | MatchesSetwise, | 33 | MatchesSetwise, |
445 | 33 | MatchesStructure, | 34 | MatchesStructure, |
446 | 34 | ) | 35 | ) |
447 | @@ -56,6 +57,8 @@ | |||
448 | 56 | from lp.services.config import config | 57 | from lp.services.config import config |
449 | 57 | from lp.services.database.constants import UTC_NOW | 58 | from lp.services.database.constants import UTC_NOW |
450 | 58 | from lp.services.features.testing import FeatureFixture | 59 | from lp.services.features.testing import FeatureFixture |
451 | 60 | from lp.services.job.interfaces.job import JobStatus | ||
452 | 61 | from lp.services.propertycache import get_property_cache | ||
453 | 59 | from lp.services.webapp import canonical_url | 62 | from lp.services.webapp import canonical_url |
454 | 60 | from lp.services.webapp.servers import LaunchpadTestRequest | 63 | from lp.services.webapp.servers import LaunchpadTestRequest |
455 | 61 | from lp.snappy.browser.snap import ( | 64 | from lp.snappy.browser.snap import ( |
456 | @@ -1344,7 +1347,7 @@ | |||
457 | 1344 | "This snap package has not been built yet.", | 1347 | "This snap package has not been built yet.", |
458 | 1345 | self.getMainText(snap)) | 1348 | self.getMainText(snap)) |
459 | 1346 | 1349 | ||
461 | 1347 | def test_index_pending(self): | 1350 | def test_index_pending_build(self): |
462 | 1348 | # A pending build is listed as such. | 1351 | # A pending build is listed as such. |
463 | 1349 | build = self.makeBuild() | 1352 | build = self.makeBuild() |
464 | 1350 | build.queueBuild() | 1353 | build.queueBuild() |
465 | @@ -1355,6 +1358,38 @@ | |||
466 | 1355 | Primary Archive for Ubuntu Linux | 1358 | Primary Archive for Ubuntu Linux |
467 | 1356 | """, self.getMainText(build.snap)) | 1359 | """, self.getMainText(build.snap)) |
468 | 1357 | 1360 | ||
469 | 1361 | def test_index_pending_build_request(self): | ||
470 | 1362 | # A pending build request is listed as such. | ||
471 | 1363 | snap = self.makeSnap() | ||
472 | 1364 | with person_logged_in(snap.owner): | ||
473 | 1365 | snap.requestBuilds( | ||
474 | 1366 | snap.owner, snap.distro_series.main_archive, | ||
475 | 1367 | PackagePublishingPocket.UPDATES) | ||
476 | 1368 | self.assertTextMatchesExpressionIgnoreWhitespace("""\ | ||
477 | 1369 | Latest builds | ||
478 | 1370 | Status When complete Architecture Archive | ||
479 | 1371 | Pending build request | ||
480 | 1372 | Primary Archive for Ubuntu Linux | ||
481 | 1373 | """, self.getMainText(snap)) | ||
482 | 1374 | |||
483 | 1375 | def test_index_failed_build_request(self): | ||
484 | 1376 | # A failed build request is listed as such, with its error message. | ||
485 | 1377 | snap = self.makeSnap() | ||
486 | 1378 | with person_logged_in(snap.owner): | ||
487 | 1379 | request = snap.requestBuilds( | ||
488 | 1380 | snap.owner, snap.distro_series.main_archive, | ||
489 | 1381 | PackagePublishingPocket.UPDATES) | ||
490 | 1382 | job = removeSecurityProxy(removeSecurityProxy(request)._job) | ||
491 | 1383 | job.job._status = JobStatus.FAILED | ||
492 | 1384 | job.job.date_finished = datetime.now(pytz.UTC) - timedelta(hours=1) | ||
493 | 1385 | job.error_message = "Boom" | ||
494 | 1386 | self.assertTextMatchesExpressionIgnoreWhitespace("""\ | ||
495 | 1387 | Latest builds | ||
496 | 1388 | Status When complete Architecture Archive | ||
497 | 1389 | Failed build request 1 hour ago \(Boom\) | ||
498 | 1390 | Primary Archive for Ubuntu Linux | ||
499 | 1391 | """, self.getMainText(snap)) | ||
500 | 1392 | |||
501 | 1358 | def test_index_store_upload(self): | 1393 | def test_index_store_upload(self): |
502 | 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, |
503 | 1360 | # the index page shows details of this. | 1395 | # the index page shows details of this. |
504 | @@ -1382,8 +1417,8 @@ | |||
505 | 1382 | build.updateStatus( | 1417 | build.updateStatus( |
506 | 1383 | status, date_finished=build.date_started + timedelta(minutes=30)) | 1418 | status, date_finished=build.date_started + timedelta(minutes=30)) |
507 | 1384 | 1419 | ||
510 | 1385 | def test_builds(self): | 1420 | def test_builds_and_requests(self): |
511 | 1386 | # SnapView.builds produces reasonable results. | 1421 | # SnapView.builds_and_requests produces reasonable results. |
512 | 1387 | snap = self.makeSnap() | 1422 | snap = self.makeSnap() |
513 | 1388 | # Create oldest builds first so that they sort properly by id. | 1423 | # Create oldest builds first so that they sort properly by id. |
514 | 1389 | date_gen = time_counter( | 1424 | date_gen = time_counter( |
515 | @@ -1392,16 +1427,67 @@ | |||
516 | 1392 | self.makeBuild(snap=snap, date_created=next(date_gen)) | 1427 | self.makeBuild(snap=snap, date_created=next(date_gen)) |
517 | 1393 | for i in range(11)] | 1428 | for i in range(11)] |
518 | 1394 | view = SnapView(snap, None) | 1429 | view = SnapView(snap, None) |
520 | 1395 | self.assertEqual(list(reversed(builds)), view.builds) | 1430 | self.assertEqual(list(reversed(builds)), view.builds_and_requests) |
521 | 1396 | self.setStatus(builds[10], BuildStatus.FULLYBUILT) | 1431 | self.setStatus(builds[10], BuildStatus.FULLYBUILT) |
522 | 1397 | self.setStatus(builds[9], BuildStatus.FAILEDTOBUILD) | 1432 | self.setStatus(builds[9], BuildStatus.FAILEDTOBUILD) |
523 | 1433 | del get_property_cache(view).builds_and_requests | ||
524 | 1398 | # When there are >= 9 pending builds, only the most recent of any | 1434 | # When there are >= 9 pending builds, only the most recent of any |
525 | 1399 | # completed builds is returned. | 1435 | # completed builds is returned. |
526 | 1400 | self.assertEqual( | 1436 | self.assertEqual( |
528 | 1401 | list(reversed(builds[:9])) + [builds[10]], view.builds) | 1437 | list(reversed(builds[:9])) + [builds[10]], |
529 | 1438 | view.builds_and_requests) | ||
530 | 1402 | for build in builds[:9]: | 1439 | for build in builds[:9]: |
531 | 1403 | self.setStatus(build, BuildStatus.FULLYBUILT) | 1440 | self.setStatus(build, BuildStatus.FULLYBUILT) |
533 | 1404 | self.assertEqual(list(reversed(builds[1:])), view.builds) | 1441 | del get_property_cache(view).builds_and_requests |
534 | 1442 | self.assertEqual(list(reversed(builds[1:])), view.builds_and_requests) | ||
535 | 1443 | |||
536 | 1444 | def test_builds_and_requests_shows_build_requests(self): | ||
537 | 1445 | # SnapView.builds_and_requests interleaves build requests with | ||
538 | 1446 | # builds. | ||
539 | 1447 | snap = self.makeSnap() | ||
540 | 1448 | date_gen = time_counter( | ||
541 | 1449 | datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1)) | ||
542 | 1450 | builds = [ | ||
543 | 1451 | self.makeBuild(snap=snap, date_created=next(date_gen)) | ||
544 | 1452 | for i in range(3)] | ||
545 | 1453 | self.setStatus(builds[2], BuildStatus.FULLYBUILT) | ||
546 | 1454 | with person_logged_in(snap.owner): | ||
547 | 1455 | request = snap.requestBuilds( | ||
548 | 1456 | snap.owner, snap.distro_series.main_archive, | ||
549 | 1457 | PackagePublishingPocket.UPDATES) | ||
550 | 1458 | job = removeSecurityProxy(removeSecurityProxy(request)._job) | ||
551 | 1459 | job.job.date_created = next(date_gen) | ||
552 | 1460 | view = SnapView(snap, None) | ||
553 | 1461 | # The pending build request is interleaved in date order with | ||
554 | 1462 | # pending builds, and these are followed by completed builds. | ||
555 | 1463 | self.assertThat(view.builds_and_requests, MatchesListwise([ | ||
556 | 1464 | MatchesStructure.byEquality(id=request.id), | ||
557 | 1465 | Equals(builds[1]), | ||
558 | 1466 | Equals(builds[0]), | ||
559 | 1467 | Equals(builds[2]), | ||
560 | 1468 | ])) | ||
561 | 1469 | transaction.commit() | ||
562 | 1470 | builds.append(self.makeBuild(snap=snap)) | ||
563 | 1471 | del get_property_cache(view).builds_and_requests | ||
564 | 1472 | self.assertThat(view.builds_and_requests, MatchesListwise([ | ||
565 | 1473 | Equals(builds[3]), | ||
566 | 1474 | MatchesStructure.byEquality(id=request.id), | ||
567 | 1475 | Equals(builds[1]), | ||
568 | 1476 | Equals(builds[0]), | ||
569 | 1477 | Equals(builds[2]), | ||
570 | 1478 | ])) | ||
571 | 1479 | # If we pretend that the job failed, it is still listed, but after | ||
572 | 1480 | # any pending builds. | ||
573 | 1481 | job.job._status = JobStatus.FAILED | ||
574 | 1482 | job.job.date_finished = job.date_created + timedelta(minutes=30) | ||
575 | 1483 | del get_property_cache(view).builds_and_requests | ||
576 | 1484 | self.assertThat(view.builds_and_requests, MatchesListwise([ | ||
577 | 1485 | Equals(builds[3]), | ||
578 | 1486 | Equals(builds[1]), | ||
579 | 1487 | Equals(builds[0]), | ||
580 | 1488 | MatchesStructure.byEquality(id=request.id), | ||
581 | 1489 | Equals(builds[2]), | ||
582 | 1490 | ])) | ||
583 | 1405 | 1491 | ||
584 | 1406 | def test_store_channels_empty(self): | 1492 | def test_store_channels_empty(self): |
585 | 1407 | snap = self.factory.makeSnap() | 1493 | snap = self.factory.makeSnap() |
586 | 1408 | 1494 | ||
587 | === modified file 'lib/lp/snappy/interfaces/snap.py' | |||
588 | --- lib/lp/snappy/interfaces/snap.py 2018-09-13 15:21:05 +0000 | |||
589 | +++ lib/lp/snappy/interfaces/snap.py 2018-10-03 00:52:37 +0000 | |||
590 | @@ -459,6 +459,11 @@ | |||
591 | 459 | value_type=Reference(ISnapBuildRequest), | 459 | value_type=Reference(ISnapBuildRequest), |
592 | 460 | required=True, readonly=True))) | 460 | required=True, readonly=True))) |
593 | 461 | 461 | ||
594 | 462 | failed_build_requests = exported(doNotSnapshot(CollectionField( | ||
595 | 463 | title=_("Failed build requests for this snap package."), | ||
596 | 464 | value_type=Reference(ISnapBuildRequest), | ||
597 | 465 | required=True, readonly=True))) | ||
598 | 466 | |||
599 | 462 | # XXX cjwatson 2018-06-20: Deprecated as an exported method; can become | 467 | # XXX cjwatson 2018-06-20: Deprecated as an exported method; can become |
600 | 463 | # an internal helper method once production JavaScript no longer uses | 468 | # an internal helper method once production JavaScript no longer uses |
601 | 464 | # it. | 469 | # it. |
602 | 465 | 470 | ||
603 | === modified file 'lib/lp/snappy/model/snap.py' | |||
604 | --- lib/lp/snappy/model/snap.py 2018-09-13 15:21:05 +0000 | |||
605 | +++ lib/lp/snappy/model/snap.py 2018-10-03 00:52:37 +0000 | |||
606 | @@ -674,7 +674,17 @@ | |||
607 | 674 | # The returned jobs are ordered by descending ID. | 674 | # The returned jobs are ordered by descending ID. |
608 | 675 | jobs = job_source.findBySnap( | 675 | jobs = job_source.findBySnap( |
609 | 676 | self, statuses=(JobStatus.WAITING, JobStatus.RUNNING)) | 676 | self, statuses=(JobStatus.WAITING, JobStatus.RUNNING)) |
611 | 677 | return [SnapBuildRequest.fromJob(job) for job in jobs] | 677 | return DecoratedResultSet( |
612 | 678 | jobs, result_decorator=SnapBuildRequest.fromJob) | ||
613 | 679 | |||
614 | 680 | @property | ||
615 | 681 | def failed_build_requests(self): | ||
616 | 682 | """See `ISnap`.""" | ||
617 | 683 | job_source = getUtility(ISnapRequestBuildsJobSource) | ||
618 | 684 | # The returned jobs are ordered by descending ID. | ||
619 | 685 | jobs = job_source.findBySnap(self, statuses=(JobStatus.FAILED,)) | ||
620 | 686 | return DecoratedResultSet( | ||
621 | 687 | jobs, result_decorator=SnapBuildRequest.fromJob) | ||
622 | 678 | 688 | ||
623 | 679 | def _getBuilds(self, filter_term, order_by): | 689 | def _getBuilds(self, filter_term, order_by): |
624 | 680 | """The actual query to get the builds.""" | 690 | """The actual query to get the builds.""" |
625 | 681 | 691 | ||
626 | === modified file 'lib/lp/snappy/templates/snap-index.pt' | |||
627 | --- lib/lp/snappy/templates/snap-index.pt 2018-09-13 15:21:05 +0000 | |||
628 | +++ lib/lp/snappy/templates/snap-index.pt 2018-10-03 00:52:37 +0000 | |||
629 | @@ -156,51 +156,62 @@ | |||
630 | 156 | </tr> | 156 | </tr> |
631 | 157 | </thead> | 157 | </thead> |
632 | 158 | <tbody> | 158 | <tbody> |
657 | 159 | <tal:snap-build-requests repeat="request context/pending_build_requests"> | 159 | <tal:snap-builds-and-requests repeat="item view/builds_and_requests"> |
658 | 160 | <tr tal:attributes="id string:request-${request/id}"> | 160 | <tal:snap-build-request condition="item/date_requested|nothing"> |
659 | 161 | <td colspan="3" | 161 | <tr tal:define="request item" |
660 | 162 | tal:attributes="class string:request_status ${request/status/name}"> | 162 | tal:attributes="id string:request-${request/id}"> |
661 | 163 | <span tal:replace="structure request/image:icon"/> | 163 | <td tal:attributes="class string:request_status ${request/status/name}"> |
662 | 164 | <tal:title replace="request/status/title"/> build request | 164 | <span tal:replace="structure request/image:icon"/> |
663 | 165 | </td> | 165 | <tal:title replace="request/status/title"/> build request |
664 | 166 | <td> | 166 | </td> |
665 | 167 | <tal:archive replace="structure request/archive/fmt:link"/> | 167 | <td> |
666 | 168 | </td> | 168 | <tal:date condition="request/date_finished" |
667 | 169 | </tr> | 169 | replace="request/date_finished/fmt:displaydate"/> |
668 | 170 | </tal:snap-build-requests> | 170 | <tal:error-message condition="request/error_message"> |
669 | 171 | <tal:snap-builds repeat="build view/builds"> | 171 | (<span tal:replace="request/error_message"/>) |
670 | 172 | <tr tal:attributes="id string:build-${build/id}"> | 172 | </tal:error-message> |
671 | 173 | <td tal:attributes="class string:build_status ${build/status/name}"> | 173 | </td> |
672 | 174 | <span tal:replace="structure build/image:icon"/> | 174 | <td/> |
673 | 175 | <a tal:content="build/status/title" | 175 | <td> |
674 | 176 | tal:attributes="href build/fmt:url"/> | 176 | <tal:archive replace="structure request/archive/fmt:link"/> |
675 | 177 | </td> | 177 | </td> |
676 | 178 | <td class="datebuilt"> | 178 | </tr> |
677 | 179 | <tal:date replace="build/date/fmt:displaydate"/> | 179 | </tal:snap-build-request> |
678 | 180 | <tal:estimate condition="build/estimate"> | 180 | <tal:snap-build condition="not: item/date_requested|nothing"> |
679 | 181 | (estimated) | 181 | <tr tal:define="build item" |
680 | 182 | </tal:estimate> | 182 | tal:attributes="id string:build-${build/id}"> |
681 | 183 | <td tal:attributes="class string:build_status ${build/status/name}"> | ||
682 | 184 | <span tal:replace="structure build/image:icon"/> | ||
683 | 185 | <a tal:content="build/status/title" | ||
684 | 186 | tal:attributes="href build/fmt:url"/> | ||
685 | 187 | </td> | ||
686 | 188 | <td class="datebuilt"> | ||
687 | 189 | <tal:date replace="build/date/fmt:displaydate"/> | ||
688 | 190 | <tal:estimate condition="build/estimate"> | ||
689 | 191 | (estimated) | ||
690 | 192 | </tal:estimate> | ||
691 | 183 | 193 | ||
709 | 184 | <tal:build-log define="file build/log" tal:condition="file"> | 194 | <tal:build-log define="file build/log" tal:condition="file"> |
710 | 185 | <a class="sprite download" | 195 | <a class="sprite download" |
711 | 186 | tal:attributes="href build/log_url">buildlog</a> | 196 | tal:attributes="href build/log_url">buildlog</a> |
712 | 187 | (<span tal:replace="file/content/filesize/fmt:bytes"/>) | 197 | (<span tal:replace="file/content/filesize/fmt:bytes"/>) |
713 | 188 | </tal:build-log> | 198 | </tal:build-log> |
714 | 189 | </td> | 199 | </td> |
715 | 190 | <td> | 200 | <td> |
716 | 191 | <a class="sprite distribution" | 201 | <a class="sprite distribution" |
717 | 192 | tal:define="archseries build/distro_arch_series" | 202 | tal:define="archseries build/distro_arch_series" |
718 | 193 | tal:attributes="href archseries/fmt:url" | 203 | tal:attributes="href archseries/fmt:url" |
719 | 194 | tal:content="archseries/architecturetag"/> | 204 | tal:content="archseries/architecturetag"/> |
720 | 195 | </td> | 205 | </td> |
721 | 196 | <td> | 206 | <td> |
722 | 197 | <tal:archive replace="structure build/archive/fmt:link"/> | 207 | <tal:archive replace="structure build/archive/fmt:link"/> |
723 | 198 | </td> | 208 | </td> |
724 | 199 | </tr> | 209 | </tr> |
725 | 200 | </tal:snap-builds> | 210 | </tal:snap-build> |
726 | 211 | </tal:snap-builds-and-requests> | ||
727 | 201 | </tbody> | 212 | </tbody> |
728 | 202 | </table> | 213 | </table> |
730 | 203 | <p tal:condition="not: view/builds"> | 214 | <p tal:condition="not: view/builds_and_requests"> |
731 | 204 | This snap package has not been built yet. | 215 | This snap package has not been built yet. |
732 | 205 | </p> | 216 | </p> |
733 | 206 | <div tal:define="link context/menu:context/request_builds" | 217 | <div tal:define="link context/menu:context/request_builds" |
734 | 207 | 218 | ||
735 | === modified file 'lib/lp/snappy/tests/test_snap.py' | |||
736 | --- lib/lp/snappy/tests/test_snap.py 2018-09-13 15:21:05 +0000 | |||
737 | +++ lib/lp/snappy/tests/test_snap.py 2018-10-03 00:52:37 +0000 | |||
738 | @@ -177,7 +177,7 @@ | |||
739 | 177 | self.assertThat( | 177 | self.assertThat( |
740 | 178 | self.factory.makeSnap(), | 178 | self.factory.makeSnap(), |
741 | 179 | DoesNotSnapshot( | 179 | DoesNotSnapshot( |
743 | 180 | ["pending_build_requests", | 180 | ["pending_build_requests", "failed_build_requests", |
744 | 181 | "builds", "completed_builds", "pending_builds"], | 181 | "builds", "completed_builds", "pending_builds"], |
745 | 182 | ISnapView)) | 182 | ISnapView)) |
746 | 183 | 183 |