Merge ~pappacena/launchpad:pkg-upload-log-api into launchpad: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)
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://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/377897) with proper prerequisite repo and branch.

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 :

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.

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
1diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
2index 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.
11diff --git a/lib/lp/registry/interfaces/persondistributionsourcepackage.py b/lib/lp/registry/interfaces/persondistributionsourcepackage.py
12index 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):
27diff --git a/lib/lp/soyuz/browser/configure.zcml b/lib/lp/soyuz/browser/configure.zcml
28index 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"
49diff --git a/lib/lp/soyuz/browser/queue.py b/lib/lp/soyuz/browser/queue.py
50index 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
143diff --git a/lib/lp/soyuz/browser/tests/test_queue.py b/lib/lp/soyuz/browser/tests/test_queue.py
144index 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)
156diff --git a/lib/lp/soyuz/interfaces/queue.py b/lib/lp/soyuz/interfaces/queue.py
157index 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
324diff --git a/lib/lp/soyuz/interfaces/webservice.py b/lib/lp/soyuz/interfaces/webservice.py
325index 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
354diff --git a/lib/lp/soyuz/model/queue.py b/lib/lp/soyuz/model/queue.py
355index 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)
422diff --git a/lib/lp/soyuz/tests/test_packageupload.py b/lib/lp/soyuz/tests/test_packageupload.py
423index 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]))