Merge ~pappacena/launchpad:pkg-upload-log-api into launchpad:master
- Git
- lp:~pappacena/launchpad
- pkg-upload-log-api
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | 3306b0830f0e41d583c33c69ee887aa3af1144dd |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~pappacena/launchpad:pkg-upload-log-api |
Merge into: | launchpad:master |
Prerequisite: | ~pappacena/launchpad:archive-queue-audit-trail |
Diff against target: |
460 lines (+124/-81) 9 files modified
lib/lp/_schema_circular_imports.py (+1/-1) lib/lp/registry/interfaces/persondistributionsourcepackage.py (+1/-1) lib/lp/soyuz/browser/configure.zcml (+6/-1) lib/lp/soyuz/browser/queue.py (+7/-36) lib/lp/soyuz/browser/tests/test_queue.py (+1/-1) lib/lp/soyuz/interfaces/queue.py (+57/-37) lib/lp/soyuz/interfaces/webservice.py (+6/-2) lib/lp/soyuz/model/queue.py (+21/-2) lib/lp/soyuz/tests/test_packageupload.py (+24/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+378124@code.launchpad.net |
Commit message
API for Package Upload logs
Description of the change
Reopening MP (https:/
To post a comment you must log in.
- 29165ff... by Thiago F. Pappacena
-
fixing import formatting
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
- 16720db... by Thiago F. Pappacena
-
Merge branch 'master' into pkg-upload-log-api
- 09ab18f... by Thiago F. Pappacena
-
minor style adjustments
Revision history for this message
Thiago F. Pappacena (pappacena) wrote : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
- 3306b08... by Thiago F. Pappacena
-
removing from interface ID attributes
Revision history for this message
Thiago F. Pappacena (pappacena) wrote : | # |
Pushing changes
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py |
2 | index 20db323..372744f 100644 |
3 | --- a/lib/lp/_schema_circular_imports.py |
4 | +++ b/lib/lp/_schema_circular_imports.py |
5 | @@ -1,4 +1,4 @@ |
6 | -# Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
7 | +# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
8 | # GNU Affero General Public License version 3 (see the file LICENSE). |
9 | |
10 | """Update the interface schema values due to circular imports. |
11 | diff --git a/lib/lp/registry/interfaces/persondistributionsourcepackage.py b/lib/lp/registry/interfaces/persondistributionsourcepackage.py |
12 | index 33ead04..d8d72bc 100644 |
13 | --- a/lib/lp/registry/interfaces/persondistributionsourcepackage.py |
14 | +++ b/lib/lp/registry/interfaces/persondistributionsourcepackage.py |
15 | @@ -16,10 +16,10 @@ from zope.interface import ( |
16 | ) |
17 | from zope.schema import TextLine |
18 | |
19 | -from lp.registry.interfaces.person import IPerson |
20 | from lp.registry.interfaces.distributionsourcepackage import ( |
21 | IDistributionSourcePackage, |
22 | ) |
23 | +from lp.registry.interfaces.person import IPerson |
24 | |
25 | |
26 | class IPersonDistributionSourcePackage(Interface): |
27 | diff --git a/lib/lp/soyuz/browser/configure.zcml b/lib/lp/soyuz/browser/configure.zcml |
28 | index 1ce6b53..4ab2e41 100644 |
29 | --- a/lib/lp/soyuz/browser/configure.zcml |
30 | +++ b/lib/lp/soyuz/browser/configure.zcml |
31 | @@ -1,4 +1,4 @@ |
32 | -<!-- Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
33 | +<!-- Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
34 | GNU Affero General Public License version 3 (see the file LICENSE). |
35 | --> |
36 | |
37 | @@ -661,6 +661,11 @@ |
38 | path_expression="string:+upload/${id}" |
39 | attribute_to_parent="distroseries" |
40 | /> |
41 | + <browser:url |
42 | + for="lp.soyuz.interfaces.queue.IPackageUploadLog" |
43 | + path_expression="string:+log/${id}" |
44 | + attribute_to_parent="package_upload" |
45 | + /> |
46 | <browser:navigation |
47 | module="lp.soyuz.browser.queue" |
48 | classes="PackageUploadNavigation" |
49 | diff --git a/lib/lp/soyuz/browser/queue.py b/lib/lp/soyuz/browser/queue.py |
50 | index 39177bd..520fd68 100644 |
51 | --- a/lib/lp/soyuz/browser/queue.py |
52 | +++ b/lib/lp/soyuz/browser/queue.py |
53 | @@ -69,7 +69,6 @@ from lp.soyuz.model.files import ( |
54 | from lp.soyuz.model.packagecopyjob import PackageCopyJob |
55 | from lp.soyuz.model.queue import ( |
56 | PackageUploadBuild, |
57 | - PackageUploadLog, |
58 | PackageUploadSource, |
59 | ) |
60 | from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease |
61 | @@ -211,26 +210,6 @@ class QueueItemsView(LaunchpadView): |
62 | list(getUtility(IPersonSet).getPrecachedPersonsFromIDs( |
63 | person_ids, need_validity=True)) |
64 | |
65 | - def _getPreloadedLogs(self, uploads): |
66 | - """Returns a dict of preloaded PackageUploadLog |
67 | - |
68 | - The keys from the returning dict are the package_upload_id, and the |
69 | - values are lists of log entries |
70 | - """ |
71 | - logs = load_referencing( |
72 | - PackageUploadLog, uploads, ['package_upload_id']) |
73 | - |
74 | - # Preload users from log entries |
75 | - # Not using `need_icon` since the log's reviewers are always persons, |
76 | - # and fetching icons should be only needed for teams |
77 | - list(getUtility(IPersonSet).getPrecachedPersonsFromIDs( |
78 | - [log.reviewer_id for log in logs], |
79 | - need_validity=True)) |
80 | - logs_dict = defaultdict(list) |
81 | - for log in logs: |
82 | - logs_dict[log.package_upload_id].append(log) |
83 | - return logs_dict |
84 | - |
85 | def decoratedQueueBatch(self): |
86 | """Return the current batch, converted to decorated objects. |
87 | |
88 | @@ -244,12 +223,12 @@ class QueueItemsView(LaunchpadView): |
89 | return None |
90 | |
91 | upload_ids = [upload.id for upload in uploads] |
92 | - puses = load_referencing( |
93 | - PackageUploadSource, uploads, ['packageuploadID']) |
94 | - pubs = load_referencing( |
95 | - PackageUploadBuild, uploads, ['packageuploadID']) |
96 | |
97 | - logs_dict = self._getPreloadedLogs(uploads) |
98 | + # Both "u.sources" and "u.builds" below are preloaded by |
99 | + # self.context.getPackageUploads (which uses PackageUploadSet.getAll) |
100 | + # when building self.batchnav. |
101 | + puses = sum([removeSecurityProxy(u.sources) for u in uploads], []) |
102 | + pubs = sum([removeSecurityProxy(u.builds) for u in uploads], []) |
103 | |
104 | source_sprs = load_related( |
105 | SourcePackageRelease, puses, ['sourcepackagereleaseID']) |
106 | @@ -288,9 +267,7 @@ class QueueItemsView(LaunchpadView): |
107 | |
108 | return [ |
109 | CompletePackageUpload( |
110 | - item, build_upload_files, source_upload_files, package_sets, |
111 | - sorted(logs_dict[item.id], key=attrgetter("date_created"), |
112 | - reverse=True)) |
113 | + item, build_upload_files, source_upload_files, package_sets) |
114 | for item in uploads] |
115 | |
116 | def is_new(self, binarypackagerelease): |
117 | @@ -517,24 +494,18 @@ class CompletePackageUpload: |
118 | # (i.e. no proxying of __set__). |
119 | pocket = None |
120 | date_created = None |
121 | - sources = None |
122 | - builds = None |
123 | - logs = None |
124 | customfiles = None |
125 | contains_source = None |
126 | contains_build = None |
127 | sourcepackagerelease = None |
128 | |
129 | def __init__(self, packageupload, build_upload_files, |
130 | - source_upload_files, package_sets, logs=None): |
131 | + source_upload_files, package_sets): |
132 | self.pocket = packageupload.pocket |
133 | self.date_created = packageupload.date_created |
134 | self.context = packageupload |
135 | - self.sources = list(packageupload.sources) |
136 | self.contains_source = len(self.sources) > 0 |
137 | - self.builds = list(packageupload.builds) |
138 | self.contains_build = len(self.builds) > 0 |
139 | - self.logs = list(logs) if logs is not None else [] |
140 | self.customfiles = list(packageupload.customfiles) |
141 | |
142 | # Create a dictionary of binary files keyed by |
143 | diff --git a/lib/lp/soyuz/browser/tests/test_queue.py b/lib/lp/soyuz/browser/tests/test_queue.py |
144 | index 242e920..0e08739 100644 |
145 | --- a/lib/lp/soyuz/browser/tests/test_queue.py |
146 | +++ b/lib/lp/soyuz/browser/tests/test_queue.py |
147 | @@ -438,7 +438,7 @@ class TestQueueItemsView(TestCaseWithFactory): |
148 | with StormStatementRecorder() as recorder: |
149 | view = self.makeView(distroseries, queue_admin) |
150 | view() |
151 | - self.assertThat(recorder, HasQueryCount(Equals(57))) |
152 | + self.assertThat(recorder, HasQueryCount(Equals(55))) |
153 | |
154 | def test_package_upload_with_logs_query_count(self): |
155 | login(ADMIN_EMAIL) |
156 | diff --git a/lib/lp/soyuz/interfaces/queue.py b/lib/lp/soyuz/interfaces/queue.py |
157 | index 929dc45..7bda98a 100644 |
158 | --- a/lib/lp/soyuz/interfaces/queue.py |
159 | +++ b/lib/lp/soyuz/interfaces/queue.py |
160 | @@ -26,6 +26,7 @@ __all__ = [ |
161 | |
162 | import httplib |
163 | |
164 | +from lazr.lifecycle.snapshot import doNotSnapshot |
165 | from lazr.restful.declarations import ( |
166 | call_with, |
167 | error_status, |
168 | @@ -37,7 +38,10 @@ from lazr.restful.declarations import ( |
169 | operation_parameters, |
170 | REQUEST_USER, |
171 | ) |
172 | -from lazr.restful.fields import Reference |
173 | +from lazr.restful.fields import ( |
174 | + CollectionField, |
175 | + Reference, |
176 | + ) |
177 | from zope.interface import ( |
178 | Attribute, |
179 | Interface, |
180 | @@ -56,6 +60,7 @@ from zope.security.interfaces import Unauthorized |
181 | from lp import _ |
182 | from lp.registry.interfaces.person import IPerson |
183 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
184 | +from lp.services.webservice.apihelpers import patch_reference_property |
185 | from lp.soyuz.enums import PackageUploadStatus |
186 | from lp.soyuz.interfaces.packagecopyjob import IPackageCopyJob |
187 | |
188 | @@ -113,6 +118,44 @@ class IPackageUploadQueue(Interface): |
189 | """ |
190 | |
191 | |
192 | +class IPackageUploadLog(Interface): |
193 | + """A log entry recording a change in a package upload's status.""" |
194 | + |
195 | + export_as_webservice_entry(publish_web_link=True, as_of="devel") |
196 | + |
197 | + id = Int(title=_('ID'), required=True, readonly=True) |
198 | + |
199 | + package_upload = exported( |
200 | + Reference( |
201 | + Interface, title=_("The package upload that generated this log"), |
202 | + required=True, readonly=True)) |
203 | + |
204 | + date_created = exported( |
205 | + Datetime( |
206 | + title=_("When this action happened."), required=True, |
207 | + readonly=True)) |
208 | + |
209 | + reviewer = exported( |
210 | + Reference( |
211 | + IPerson, title=_("Who did this action."), |
212 | + required=True, readonly=True)) |
213 | + |
214 | + old_status = exported( |
215 | + Choice( |
216 | + vocabulary=PackageUploadStatus, description=_("Old status."), |
217 | + required=True, readonly=True)) |
218 | + |
219 | + new_status = exported( |
220 | + Choice( |
221 | + vocabulary=PackageUploadStatus, description=_("New status."), |
222 | + required=True, readonly=True)) |
223 | + |
224 | + comment = exported( |
225 | + TextLine( |
226 | + title=_("User's comment about this change."), |
227 | + required=False, readonly=True)) |
228 | + |
229 | + |
230 | class IPackageUpload(Interface): |
231 | """A Queue item for the archive uploader.""" |
232 | |
233 | @@ -151,8 +194,6 @@ class IPackageUpload(Interface): |
234 | title=_('Date created'), |
235 | description=_("The date this package upload was done."))) |
236 | |
237 | - logs = Attribute(_("The change log of this PackageUpload.")) |
238 | - |
239 | changesfile = Attribute("The librarian alias for the changes file " |
240 | "associated with this upload") |
241 | changes_file_url = exported( |
242 | @@ -185,6 +226,14 @@ class IPackageUpload(Interface): |
243 | sources = Attribute("The queue sources associated with this queue item") |
244 | builds = Attribute("The queue builds associated with the queue item") |
245 | |
246 | + logs = exported( |
247 | + doNotSnapshot( |
248 | + CollectionField( |
249 | + title=_("The package upload logs"), |
250 | + value_type=Reference(schema=IPackageUploadLog), |
251 | + readonly=True)), |
252 | + as_of="devel") |
253 | + |
254 | customfiles = Attribute("Custom upload files associated with this " |
255 | "queue item") |
256 | custom_file_urls = exported( |
257 | @@ -495,6 +544,9 @@ class IPackageUpload(Interface): |
258 | """ |
259 | |
260 | |
261 | +patch_reference_property(IPackageUploadLog, 'package_upload', IPackageUpload) |
262 | + |
263 | + |
264 | class IPackageUploadBuild(Interface): |
265 | """A Queue item's related builds.""" |
266 | |
267 | @@ -507,9 +559,7 @@ class IPackageUploadBuild(Interface): |
268 | readonly=False, |
269 | ) |
270 | |
271 | - build = Int( |
272 | - title=_("The related build"), required=True, readonly=False, |
273 | - ) |
274 | + build = Int(title=_("The related build"), required=True, readonly=False) |
275 | |
276 | def binaries(): |
277 | """Returns the properties of the binaries in this build. |
278 | @@ -552,8 +602,7 @@ class IPackageUploadSource(Interface): |
279 | |
280 | sourcepackagerelease = Int( |
281 | title=_("The related source package release"), required=True, |
282 | - readonly=False, |
283 | - ) |
284 | + readonly=False) |
285 | |
286 | def getSourceAncestryForDiffs(): |
287 | """Return a suitable ancestry publication for this context. |
288 | @@ -715,35 +764,6 @@ class IPackageUploadCustom(Interface): |
289 | """ |
290 | |
291 | |
292 | -class IPackageUploadLog(Interface): |
293 | - """Entries of package upload status change""" |
294 | - |
295 | - id = Int(title=_('ID'), required=True, readonly=True) |
296 | - |
297 | - package_upload = Reference( |
298 | - IPackageUpload, |
299 | - title=_("Original package upload."), required=True, readonly=True) |
300 | - |
301 | - date_created = Datetime( |
302 | - title=_("When this action happened."), required=True, readonly=True) |
303 | - |
304 | - reviewer = Reference( |
305 | - IPerson, title=_("Who did this action."), |
306 | - required=True, readonly=True) |
307 | - |
308 | - old_status = Choice( |
309 | - vocabulary=PackageUploadStatus, description=_("Old status."), |
310 | - required=True, readonly=True) |
311 | - |
312 | - new_status = Choice( |
313 | - vocabulary=PackageUploadStatus, description=_("New status."), |
314 | - required=True, readonly=True) |
315 | - |
316 | - comment = TextLine( |
317 | - title=_("User's comment about this change."), |
318 | - required=False, readonly=True) |
319 | - |
320 | - |
321 | class IPackageUploadSet(Interface): |
322 | """Represents a set of IPackageUploads""" |
323 | |
324 | diff --git a/lib/lp/soyuz/interfaces/webservice.py b/lib/lp/soyuz/interfaces/webservice.py |
325 | index 3620e81..90ef0e1 100644 |
326 | --- a/lib/lp/soyuz/interfaces/webservice.py |
327 | +++ b/lib/lp/soyuz/interfaces/webservice.py |
328 | @@ -1,4 +1,4 @@ |
329 | -# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
330 | +# Copyright 2010-2020 Canonical Ltd. This software is licensed under the |
331 | # GNU Affero General Public License version 3 (see the file LICENSE). |
332 | |
333 | """All the interfaces that are exposed through the webservice. |
334 | @@ -35,6 +35,7 @@ __all__ = [ |
335 | 'ILiveFSBuild', |
336 | 'ILiveFSSet', |
337 | 'IPackageUpload', |
338 | + 'IPackageUploadLog', |
339 | 'IPackageset', |
340 | 'IPackagesetSet', |
341 | 'ISourcePackagePublishingHistory', |
342 | @@ -104,7 +105,10 @@ from lp.soyuz.interfaces.publishing import ( |
343 | IBinaryPackagePublishingHistory, |
344 | ISourcePackagePublishingHistory, |
345 | ) |
346 | -from lp.soyuz.interfaces.queue import IPackageUpload |
347 | +from lp.soyuz.interfaces.queue import ( |
348 | + IPackageUpload, |
349 | + IPackageUploadLog, |
350 | + ) |
351 | |
352 | |
353 | _schema_circular_imports |
354 | diff --git a/lib/lp/soyuz/model/queue.py b/lib/lp/soyuz/model/queue.py |
355 | index 6a55fa7..91e92cd 100644 |
356 | --- a/lib/lp/soyuz/model/queue.py |
357 | +++ b/lib/lp/soyuz/model/queue.py |
358 | @@ -12,7 +12,9 @@ __all__ = [ |
359 | 'PackageUploadSource', |
360 | ] |
361 | |
362 | +from collections import defaultdict |
363 | from itertools import chain |
364 | +from operator import attrgetter |
365 | |
366 | import pytz |
367 | from sqlobject import ( |
368 | @@ -42,6 +44,7 @@ from zope.interface import implementer |
369 | from lp.app.errors import NotFoundError |
370 | from lp.archiveuploader.tagfiles import parse_tagfile_content |
371 | from lp.registry.interfaces.gpg import IGPGKeySet |
372 | +from lp.registry.interfaces.person import IPersonSet |
373 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
374 | from lp.registry.model.sourcepackagename import SourcePackageName |
375 | from lp.services.auditor.client import AuditorClient |
376 | @@ -1600,8 +1603,10 @@ class PackageUploadSet: |
377 | PackageUploadBuild, rows, ["packageuploadID"]) |
378 | pucs = load_referencing( |
379 | PackageUploadCustom, rows, ["packageuploadID"]) |
380 | + logs = load_referencing( |
381 | + PackageUploadLog, rows, ["package_upload_id"]) |
382 | |
383 | - prefill_packageupload_caches(rows, puses, pubs, pucs) |
384 | + prefill_packageupload_caches(rows, puses, pubs, pucs, logs) |
385 | |
386 | return DecoratedResultSet(query, pre_iter_hook=preload_hook) |
387 | |
388 | @@ -1622,18 +1627,32 @@ class PackageUploadSet: |
389 | PackageUpload.package_copy_job_id.is_in(pcj_ids)) |
390 | |
391 | |
392 | -def prefill_packageupload_caches(uploads, puses, pubs, pucs): |
393 | +def prefill_packageupload_caches(uploads, puses, pubs, pucs, logs): |
394 | # Circular imports. |
395 | from lp.soyuz.model.archive import Archive |
396 | from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild |
397 | from lp.soyuz.model.publishing import SourcePackagePublishingHistory |
398 | from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease |
399 | |
400 | + logs_per_pu = defaultdict(list) |
401 | + reviewer_ids = set() |
402 | + for log in logs: |
403 | + reviewer_ids.add(log.reviewer_id) |
404 | + logs_per_pu[log.package_upload_id].append(log) |
405 | + |
406 | + # Preload reviewers of the logs. |
407 | + # We are not using `need_icon` here because reviewers are persons, |
408 | + # and icons are only available for teams. |
409 | + list(getUtility(IPersonSet).getPrecachedPersonsFromIDs( |
410 | + reviewer_ids, need_validity=True)) |
411 | + |
412 | for pu in uploads: |
413 | cache = get_property_cache(pu) |
414 | cache.sources = [] |
415 | cache.builds = [] |
416 | cache.customfiles = [] |
417 | + cache.logs = sorted( |
418 | + logs_per_pu[pu.id], key=attrgetter("date_created"), reverse=True) |
419 | |
420 | for pus in puses: |
421 | get_property_cache(pus.packageupload).sources.append(pus) |
422 | diff --git a/lib/lp/soyuz/tests/test_packageupload.py b/lib/lp/soyuz/tests/test_packageupload.py |
423 | index 0c83f33..6e78cdf 100644 |
424 | --- a/lib/lp/soyuz/tests/test_packageupload.py |
425 | +++ b/lib/lp/soyuz/tests/test_packageupload.py |
426 | @@ -18,6 +18,7 @@ from lazr.restfulclient.errors import ( |
427 | ) |
428 | from testtools.matchers import ( |
429 | Equals, |
430 | + MatchesListwise, |
431 | MatchesStructure, |
432 | ) |
433 | import transaction |
434 | @@ -1483,3 +1484,26 @@ class TestPackageUploadWebservice(TestCaseWithFactory): |
435 | person, component=self.universe), |
436 | 5) |
437 | self.assertThat(recorder2, HasQueryCount.byEquality(recorder1)) |
438 | + |
439 | + def test_api_package_upload_log(self): |
440 | + # API clients can see upload logs of a source uploads. |
441 | + admin = self.makeQueueAdmin([self.universe]) |
442 | + upload, ws_upload = self.makeSourcePackageUpload( |
443 | + admin, sourcepackagename="hello", component=self.universe) |
444 | + with person_logged_in(admin): |
445 | + upload.rejectFromQueue(admin, 'not a good change') |
446 | + upload.acceptFromQueue(admin) |
447 | + |
448 | + logs = removeSecurityProxy(upload).logs |
449 | + ws_logs = ws_upload.logs |
450 | + self.assertEqual(len(ws_logs), len(logs)) |
451 | + self.assertThat(ws_upload.logs, MatchesListwise([ |
452 | + MatchesStructure( |
453 | + comment=Equals(log.comment), |
454 | + date_created=Equals(log.date_created), |
455 | + new_status=Equals(log.new_status.title), |
456 | + old_status=Equals(log.old_status.title), |
457 | + reviewer=MatchesStructure.byEquality( |
458 | + name=log.reviewer.name), |
459 | + package_upload=MatchesStructure.byEquality(id=ws_upload.id)) |
460 | + for log in removeSecurityProxy(upload).logs])) |
I'm pushing the changes. There is still one open topic about having the "ID" attributes at interface level that I would like your feedback, cjwatson.