← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/launchpad:pkg-upload-log-api into launchpad:master

 

Thiago F. Pappacena has proposed merging ~pappacena/launchpad:pkg-upload-log-api into launchpad:master with ~pappacena/launchpad:archive-queue-audit-trail as a prerequisite.

Commit message:
API for Package Upload logs

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/378124

Reopening MP (https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/377897) with proper prerequisite repo and branch.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:pkg-upload-log-api into launchpad:master.
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index 20db323..49b37ec 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -206,7 +206,7 @@ from lp.soyuz.interfaces.publishing import (
     ISourcePackagePublishingHistoryEdit,
     ISourcePackagePublishingHistoryPublic,
     )
-from lp.soyuz.interfaces.queue import IPackageUpload
+from lp.soyuz.interfaces.queue import IPackageUpload, IPackageUploadLog
 from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
 from lp.translations.interfaces.hastranslationimports import (
     IHasTranslationImports,
@@ -363,6 +363,8 @@ patch_reference_property(
     ISourcePackagePublishingHistory)
 patch_reference_property(
     ISourcePackagePublishingHistory, 'packageupload', IPackageUpload)
+patch_reference_property(
+    IPackageUploadLog, 'package_upload', IPackageUpload)
 patch_entry_return_type(
     ISourcePackagePublishingHistoryEdit, 'changeOverride',
     ISourcePackagePublishingHistory)
diff --git a/lib/lp/soyuz/browser/configure.zcml b/lib/lp/soyuz/browser/configure.zcml
index 1ce6b53..4ab2e41 100644
--- a/lib/lp/soyuz/browser/configure.zcml
+++ b/lib/lp/soyuz/browser/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -661,6 +661,11 @@
         path_expression="string:+upload/${id}"
         attribute_to_parent="distroseries"
         />
+    <browser:url
+        for="lp.soyuz.interfaces.queue.IPackageUploadLog"
+        path_expression="string:+log/${id}"
+        attribute_to_parent="package_upload"
+        />
     <browser:navigation
         module="lp.soyuz.browser.queue"
         classes="PackageUploadNavigation"
diff --git a/lib/lp/soyuz/browser/queue.py b/lib/lp/soyuz/browser/queue.py
index 39177bd..02d5f54 100644
--- a/lib/lp/soyuz/browser/queue.py
+++ b/lib/lp/soyuz/browser/queue.py
@@ -69,7 +69,6 @@ from lp.soyuz.model.files import (
 from lp.soyuz.model.packagecopyjob import PackageCopyJob
 from lp.soyuz.model.queue import (
     PackageUploadBuild,
-    PackageUploadLog,
     PackageUploadSource,
     )
 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
@@ -211,26 +210,6 @@ class QueueItemsView(LaunchpadView):
         list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
             person_ids, need_validity=True))
 
-    def _getPreloadedLogs(self, uploads):
-        """Returns a dict of preloaded PackageUploadLog
-
-        The keys from the returning dict are the package_upload_id, and the
-        values are lists of log entries
-        """
-        logs = load_referencing(
-            PackageUploadLog, uploads, ['package_upload_id'])
-
-        # Preload users from log entries
-        # Not using `need_icon` since the log's reviewers are always persons,
-        # and fetching icons should be only needed for teams
-        list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
-            [log.reviewer_id for log in logs],
-            need_validity=True))
-        logs_dict = defaultdict(list)
-        for log in logs:
-            logs_dict[log.package_upload_id].append(log)
-        return logs_dict
-
     def decoratedQueueBatch(self):
         """Return the current batch, converted to decorated objects.
 
@@ -244,12 +223,8 @@ class QueueItemsView(LaunchpadView):
             return None
 
         upload_ids = [upload.id for upload in uploads]
-        puses = load_referencing(
-            PackageUploadSource, uploads, ['packageuploadID'])
-        pubs = load_referencing(
-            PackageUploadBuild, uploads, ['packageuploadID'])
-
-        logs_dict = self._getPreloadedLogs(uploads)
+        puses = sum([u.sources for u in uploads], [])
+        pubs = sum([u.builds for u in uploads], [])
 
         source_sprs = load_related(
             SourcePackageRelease, puses, ['sourcepackagereleaseID'])
@@ -288,9 +263,7 @@ class QueueItemsView(LaunchpadView):
 
         return [
             CompletePackageUpload(
-                item, build_upload_files, source_upload_files, package_sets,
-                sorted(logs_dict[item.id], key=attrgetter("date_created"),
-                       reverse=True))
+                item, build_upload_files, source_upload_files, package_sets)
             for item in uploads]
 
     def is_new(self, binarypackagerelease):
@@ -517,24 +490,18 @@ class CompletePackageUpload:
     # (i.e. no proxying of __set__).
     pocket = None
     date_created = None
-    sources = None
-    builds = None
-    logs = None
     customfiles = None
     contains_source = None
     contains_build = None
     sourcepackagerelease = None
 
     def __init__(self, packageupload, build_upload_files,
-                 source_upload_files, package_sets, logs=None):
+                 source_upload_files, package_sets):
         self.pocket = packageupload.pocket
         self.date_created = packageupload.date_created
         self.context = packageupload
-        self.sources = list(packageupload.sources)
         self.contains_source = len(self.sources) > 0
-        self.builds = list(packageupload.builds)
         self.contains_build = len(self.builds) > 0
-        self.logs = list(logs) if logs is not None else []
         self.customfiles = list(packageupload.customfiles)
 
         # Create a dictionary of binary files keyed by
diff --git a/lib/lp/soyuz/browser/tests/test_queue.py b/lib/lp/soyuz/browser/tests/test_queue.py
index 242e920..0e08739 100644
--- a/lib/lp/soyuz/browser/tests/test_queue.py
+++ b/lib/lp/soyuz/browser/tests/test_queue.py
@@ -438,7 +438,7 @@ class TestQueueItemsView(TestCaseWithFactory):
             with StormStatementRecorder() as recorder:
                 view = self.makeView(distroseries, queue_admin)
                 view()
-        self.assertThat(recorder, HasQueryCount(Equals(57)))
+        self.assertThat(recorder, HasQueryCount(Equals(55)))
 
     def test_package_upload_with_logs_query_count(self):
         login(ADMIN_EMAIL)
diff --git a/lib/lp/soyuz/interfaces/queue.py b/lib/lp/soyuz/interfaces/queue.py
index 929dc45..3a02689 100644
--- a/lib/lp/soyuz/interfaces/queue.py
+++ b/lib/lp/soyuz/interfaces/queue.py
@@ -26,6 +26,7 @@ __all__ = [
 
 import httplib
 
+from lazr.lifecycle.snapshot import doNotSnapshot
 from lazr.restful.declarations import (
     call_with,
     error_status,
@@ -37,7 +38,10 @@ from lazr.restful.declarations import (
     operation_parameters,
     REQUEST_USER,
     )
-from lazr.restful.fields import Reference
+from lazr.restful.fields import (
+    CollectionField,
+    Reference,
+    )
 from zope.interface import (
     Attribute,
     Interface,
@@ -113,6 +117,44 @@ class IPackageUploadQueue(Interface):
     """
 
 
+class IPackageUploadLog(Interface):
+    """A log entry recording a change in a package upload's status."""
+
+    export_as_webservice_entry(publish_web_link=True, as_of="devel")
+
+    id = Int(title=_('ID'), required=True, readonly=True)
+
+    package_upload = exported(
+        Reference(
+            Interface, title=_("The package upload that generated this log"),
+            required=True, readonly=True))
+
+    date_created = exported(
+        Datetime(
+            title=_("When this action happened."), required=True,
+            readonly=True))
+
+    reviewer = exported(
+        Reference(
+            IPerson, title=_("Who did this action."),
+            required=True, readonly=True))
+
+    old_status = exported(
+        Choice(
+            vocabulary=PackageUploadStatus, description=_("Old status."),
+            required=True, readonly=True))
+
+    new_status = exported(
+        Choice(
+            vocabulary=PackageUploadStatus, description=_("New status."),
+            required=True, readonly=True))
+
+    comment = exported(
+        TextLine(
+            title=_("User's comment about this change."),
+            required=False, readonly=True))
+
+
 class IPackageUpload(Interface):
     """A Queue item for the archive uploader."""
 
@@ -151,8 +193,6 @@ class IPackageUpload(Interface):
             title=_('Date created'),
             description=_("The date this package upload was done.")))
 
-    logs = Attribute(_("The change log of this PackageUpload."))
-
     changesfile = Attribute("The librarian alias for the changes file "
                             "associated with this upload")
     changes_file_url = exported(
@@ -185,6 +225,14 @@ class IPackageUpload(Interface):
     sources = Attribute("The queue sources associated with this queue item")
     builds = Attribute("The queue builds associated with the queue item")
 
+    logs = exported(
+        doNotSnapshot(
+            CollectionField(
+                title=_("The package upload logs"),
+                value_type=Reference(schema=IPackageUploadLog),
+                readonly=True)),
+        as_of="devel")
+
     customfiles = Attribute("Custom upload files associated with this "
                             "queue item")
     custom_file_urls = exported(
@@ -507,9 +555,11 @@ class IPackageUploadBuild(Interface):
             readonly=False,
             )
 
-    build = Int(
-            title=_("The related build"), required=True, readonly=False,
-            )
+    build = Int(title=_("The related build"), required=True, readonly=False)
+
+    buildID = Int(
+        title=_("The Related build ID"), required=True,
+        readonly=True)
 
     def binaries():
         """Returns the properties of the binaries in this build.
@@ -552,8 +602,11 @@ class IPackageUploadSource(Interface):
 
     sourcepackagerelease = Int(
             title=_("The related source package release"), required=True,
-            readonly=False,
-            )
+            readonly=False)
+
+    sourcepackagereleaseID = Int(
+            title=_("The related source package release ID"), required=True,
+            readonly=True)
 
     def getSourceAncestryForDiffs():
         """Return a suitable ancestry publication for this context.
@@ -715,35 +768,6 @@ class IPackageUploadCustom(Interface):
         """
 
 
-class IPackageUploadLog(Interface):
-    """Entries of package upload status change"""
-
-    id = Int(title=_('ID'), required=True, readonly=True)
-
-    package_upload = Reference(
-        IPackageUpload,
-        title=_("Original package upload."), required=True, readonly=True)
-
-    date_created = Datetime(
-        title=_("When this action happened."), required=True, readonly=True)
-
-    reviewer = Reference(
-        IPerson, title=_("Who did this action."),
-        required=True, readonly=True)
-
-    old_status = Choice(
-        vocabulary=PackageUploadStatus, description=_("Old status."),
-        required=True, readonly=True)
-
-    new_status = Choice(
-        vocabulary=PackageUploadStatus, description=_("New status."),
-        required=True, readonly=True)
-
-    comment = TextLine(
-        title=_("User's comment about this change."),
-        required=False, readonly=True)
-
-
 class IPackageUploadSet(Interface):
     """Represents a set of IPackageUploads"""
 
diff --git a/lib/lp/soyuz/interfaces/webservice.py b/lib/lp/soyuz/interfaces/webservice.py
index 3620e81..90ef0e1 100644
--- a/lib/lp/soyuz/interfaces/webservice.py
+++ b/lib/lp/soyuz/interfaces/webservice.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2019 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """All the interfaces that are exposed through the webservice.
@@ -35,6 +35,7 @@ __all__ = [
     'ILiveFSBuild',
     'ILiveFSSet',
     'IPackageUpload',
+    'IPackageUploadLog',
     'IPackageset',
     'IPackagesetSet',
     'ISourcePackagePublishingHistory',
@@ -104,7 +105,10 @@ from lp.soyuz.interfaces.publishing import (
     IBinaryPackagePublishingHistory,
     ISourcePackagePublishingHistory,
     )
-from lp.soyuz.interfaces.queue import IPackageUpload
+from lp.soyuz.interfaces.queue import (
+    IPackageUpload,
+    IPackageUploadLog,
+    )
 
 
 _schema_circular_imports
diff --git a/lib/lp/soyuz/model/queue.py b/lib/lp/soyuz/model/queue.py
index 6a55fa7..91e92cd 100644
--- a/lib/lp/soyuz/model/queue.py
+++ b/lib/lp/soyuz/model/queue.py
@@ -12,7 +12,9 @@ __all__ = [
     'PackageUploadSource',
     ]
 
+from collections import defaultdict
 from itertools import chain
+from operator import attrgetter
 
 import pytz
 from sqlobject import (
@@ -42,6 +44,7 @@ from zope.interface import implementer
 from lp.app.errors import NotFoundError
 from lp.archiveuploader.tagfiles import parse_tagfile_content
 from lp.registry.interfaces.gpg import IGPGKeySet
+from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.model.sourcepackagename import SourcePackageName
 from lp.services.auditor.client import AuditorClient
@@ -1600,8 +1603,10 @@ class PackageUploadSet:
                 PackageUploadBuild, rows, ["packageuploadID"])
             pucs = load_referencing(
                 PackageUploadCustom, rows, ["packageuploadID"])
+            logs = load_referencing(
+                PackageUploadLog, rows, ["package_upload_id"])
 
-            prefill_packageupload_caches(rows, puses, pubs, pucs)
+            prefill_packageupload_caches(rows, puses, pubs, pucs, logs)
 
         return DecoratedResultSet(query, pre_iter_hook=preload_hook)
 
@@ -1622,18 +1627,32 @@ class PackageUploadSet:
             PackageUpload.package_copy_job_id.is_in(pcj_ids))
 
 
-def prefill_packageupload_caches(uploads, puses, pubs, pucs):
+def prefill_packageupload_caches(uploads, puses, pubs, pucs, logs):
     # Circular imports.
     from lp.soyuz.model.archive import Archive
     from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
     from lp.soyuz.model.publishing import SourcePackagePublishingHistory
     from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
 
+    logs_per_pu = defaultdict(list)
+    reviewer_ids = set()
+    for log in logs:
+        reviewer_ids.add(log.reviewer_id)
+        logs_per_pu[log.package_upload_id].append(log)
+
+    # Preload reviewers of the logs.
+    # We are not using `need_icon` here because reviewers are persons,
+    # and icons are only available for teams.
+    list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+        reviewer_ids, need_validity=True))
+
     for pu in uploads:
         cache = get_property_cache(pu)
         cache.sources = []
         cache.builds = []
         cache.customfiles = []
+        cache.logs = sorted(
+            logs_per_pu[pu.id], key=attrgetter("date_created"), reverse=True)
 
     for pus in puses:
         get_property_cache(pus.packageupload).sources.append(pus)
diff --git a/lib/lp/soyuz/tests/test_packageupload.py b/lib/lp/soyuz/tests/test_packageupload.py
index 0c83f33..1e7e655 100644
--- a/lib/lp/soyuz/tests/test_packageupload.py
+++ b/lib/lp/soyuz/tests/test_packageupload.py
@@ -1483,3 +1483,23 @@ class TestPackageUploadWebservice(TestCaseWithFactory):
                 person, component=self.universe),
             5)
         self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
+
+    def test_api_package_upload_log(self):
+        # API clients can see upload logs of a source uploads.
+        admin = self.makeQueueAdmin([self.universe])
+        upload, ws_upload = self.makeSourcePackageUpload(
+            admin, sourcepackagename="hello", component=self.universe)
+        with person_logged_in(admin):
+            upload.rejectFromQueue(admin, 'not a good change')
+            upload.acceptFromQueue(admin)
+
+        logs = removeSecurityProxy(upload).logs
+        ws_logs = ws_upload.logs
+        self.assertEqual(len(ws_logs), len(logs))
+        for log, ws_log in zip(logs, ws_logs):
+            self.assertEqual(log.comment, ws_log.comment)
+            self.assertEqual(log.date_created, ws_log.date_created)
+            self.assertEqual(log.new_status.title, ws_log.new_status)
+            self.assertEqual(log.old_status.title, ws_log.old_status)
+            self.assertEqual(log.reviewer.name, ws_log.reviewer.name)
+            self.assertEqual(ws_log.package_upload.id, ws_upload.id)