Thiago F. Pappacena has proposed merging ~pappacena/launchpad:archive-queue-audit-trail into launchpad:master.
Commit message: Creating audit logs for package approval and rejection Requested reviews: Launchpad code reviewers (launchpad-reviewers) Related bugs: Bug #885739 in Launchpad itself: "queue and override manipulations should have an audit trail" https://bugs.launchpad.net/launchpad/+bug/885739 For more details, see: https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/377717 -- Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:archive-queue-audit-trail into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg index 06da0d4..2844a30 100644 --- a/database/schema/security.cfg +++ b/database/schema/security.cfg @@ -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). # # Possible permissions: SELECT, INSERT, UPDATE, EXECUTE @@ -1080,6 +1080,7 @@ public.packagesetgroup = SELECT, INSERT public.packagesetinclusion = SELECT, INSERT public.packagesetsources = SELECT, INSERT public.packageupload = SELECT, INSERT, UPDATE +public.packageuploadlog = SELECT public.packageuploadsource = SELECT public.packageuploadbuild = SELECT public.packageuploadcustom = SELECT, INSERT @@ -1242,6 +1243,7 @@ public.ociprojectname = SELECT, INSERT, UPDATE public.ociprojectseries = SELECT, INSERT, UPDATE, DELETE public.openididentifier = SELECT public.packageupload = SELECT, INSERT, UPDATE +public.packageuploadlog = SELECT, INSERT public.packageuploadbuild = SELECT, INSERT, UPDATE public.packageuploadcustom = SELECT, INSERT, UPDATE public.packageuploadsource = SELECT, INSERT, UPDATE @@ -2293,6 +2295,7 @@ public.packagediff = SELECT, UPDATE public.packageset = SELECT, UPDATE public.packagesetgroup = SELECT, UPDATE public.packageupload = SELECT, UPDATE +public.packageuploadlog = SELECT, UPDATE public.packaging = SELECT, UPDATE public.person = SELECT, UPDATE public.personlanguage = SELECT, UPDATE diff --git a/lib/lp/soyuz/configure.zcml b/lib/lp/soyuz/configure.zcml index 0e35ccb..f159d27 100644 --- a/lib/lp/soyuz/configure.zcml +++ b/lib/lp/soyuz/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). --> @@ -151,6 +151,7 @@ displayarchs displayversion isPPA + logs package_copy_job package_copy_job_id package_name @@ -183,6 +184,11 @@ set_attributes="status distroseries pocket changesfile archive"/> </class> <class + class="lp.soyuz.model.queue.PackageUploadLog"> + <allow + interface="lp.soyuz.interfaces.queue.IPackageUploadLog"/> + </class> + <class class="lp.soyuz.model.queue.PackageUploadSource"> <allow interface="lp.soyuz.interfaces.queue.IPackageUploadSource"/> diff --git a/lib/lp/soyuz/interfaces/queue.py b/lib/lp/soyuz/interfaces/queue.py index c57da73..3b44a13 100644 --- a/lib/lp/soyuz/interfaces/queue.py +++ b/lib/lp/soyuz/interfaces/queue.py @@ -11,6 +11,7 @@ __all__ = [ 'IHasQueueItems', 'IPackageUploadQueue', 'IPackageUpload', + 'IPackageUploadLog', 'IPackageUploadBuild', 'IPackageUploadSource', 'IPackageUploadCustom', @@ -149,6 +150,8 @@ 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( @@ -711,6 +714,20 @@ class IPackageUploadCustom(Interface): """ +class IPackageUploadLog(Interface): + package_upload = Attribute("Original package upload") + + date_created = Attribute("When this action happened") + + person = Attribute("Who did this action") + + old_status = Attribute("Old status") + + new_status = Attribute("New status") + + comment = Attribute("User's comment about this change") + + class IPackageUploadSet(Interface): """Represents a set of IPackageUploads""" diff --git a/lib/lp/soyuz/model/queue.py b/lib/lp/soyuz/model/queue.py index 0fd1799..fcfdc2c 100644 --- a/lib/lp/soyuz/model/queue.py +++ b/lib/lp/soyuz/model/queue.py @@ -1,10 +1,11 @@ -# Copyright 2009-2018 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). __metaclass__ = type __all__ = [ 'PackageUploadQueue', 'PackageUpload', + 'PackageUploadLog', 'PackageUploadBuild', 'PackageUploadSource', 'PackageUploadCustom', @@ -13,6 +14,7 @@ __all__ = [ from itertools import chain +import pytz from sqlobject import ( ForeignKey, SQLMultipleJoin, @@ -29,6 +31,8 @@ from storm.locals import ( SQL, Unicode, ) +from storm.properties import DateTime +from storm.references import ReferenceSet from storm.store import ( EmptyResultSet, Store, @@ -106,7 +110,7 @@ from lp.soyuz.interfaces.queue import ( QueueInconsistentStateError, QueueSourceAcceptError, QueueStateWriteProtectedError, - ) + IPackageUploadLog) from lp.soyuz.interfaces.section import ISectionSet from lp.soyuz.mail.packageupload import PackageUploadMailer from lp.soyuz.model.binarypackagename import BinaryPackageName @@ -204,6 +208,20 @@ class PackageUpload(SQLBase): self.addSearchableNames([self.package_copy_job.package_name]) self.addSearchableVersions([self.package_copy_job.package_version]) + @property + def logs(self): + return Store.of(self).find(PackageUploadLog, + PackageUploadLog.package_upload == self) + + def _addLog(self, person, new_status, comment=None): + return Store.of(self).add(PackageUploadLog( + package_upload=self, + person=person, + old_status=self.status, + new_status=new_status, + comment=comment + )) + @cachedproperty def sources(self): return list(self._sources) @@ -571,6 +589,7 @@ class PackageUpload(SQLBase): def acceptFromQueue(self, user=None): """See `IPackageUpload`.""" + self._addLog(user, PackageUploadStatus.ACCEPTED, None) if self.package_copy_job is None: self._acceptNonSyncFromQueue() else: @@ -581,6 +600,7 @@ class PackageUpload(SQLBase): def rejectFromQueue(self, user, comment=None): """See `IPackageUpload`.""" + self._addLog(user, PackageUploadStatus.REJECTED, comment) self.setRejected() if self.package_copy_job is not None: # Circular imports :( @@ -1153,6 +1173,24 @@ def get_properties_for_binary(bpr): } +@implementer(IPackageUploadLog) +class PackageUploadLog(SQLBase): + package_upload_id = Int(name='package_upload') + package_upload = Reference(package_upload_id, PackageUpload.id) + + date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, + default=UTC_NOW) + + person_id = Int(name='person', allow_none=True) + person = Reference(person_id, 'Person.id') + + old_status = EnumCol(schema=PackageUploadStatus) + + new_status = EnumCol(schema=PackageUploadStatus) + + comment = Unicode(allow_none=True) + + @implementer(IPackageUploadBuild) class PackageUploadBuild(SQLBase): """A Queue item's related builds.""" diff --git a/lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt b/lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt index 7417a7b..1a5f2cc 100644 --- a/lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt +++ b/lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt @@ -510,7 +510,7 @@ values: >>> filelist = find_tags_by_class(anon_browser.contents, 'queue-2') >>> for row in filelist: ... print(extract_text(row)) - pmount_1.0-1_all.deb (18 bytes) NEW 0.1-1 restricted admin extra + pmount_1.0-1_all.deb (18 bytes) NEW 0.1-1 restricted admin extra Accepted a moment ago by Sample Person 'netapplet' has gone straight to the 'done' queue because it's a single source upload, and we can see its overridden values there: @@ -546,6 +546,15 @@ Rejecting 'alsa-utils' source: netapplet...ddtp... - Release 2006-... netapplet...dist... - Release 2006-... + >>> upload_manager_browser.getControl( + ... name="queue_state", index=0).displayValue=['Rejected'] + >>> upload_manager_browser.getControl("Update").click() + >>> logs = find_tags_by_class( + ... upload_manager_browser.contents, "log-content") + >>> for log in logs: + ... print(extract_text(log)) + Rejected...a moment ago...by Sample Person...Foo + One rejection email is generated: >>> run_package_upload_notification_jobs() @@ -599,6 +608,20 @@ button will be disabled. >>> upload_manager_browser.getControl(name="Reject").disabled True +Accepting alsa again, and check that the package upload log has more rows + + >>> upload_manager_browser.getControl(name="QUEUE_ID").value = ['4'] + >>> upload_manager_browser.getControl(name="Accept").click() + >>> upload_manager_browser.getControl( + ... name="queue_state", index=0).displayValue=['Accepted'] + >>> upload_manager_browser.getControl("Update").click() + >>> logs = find_tags_by_class( + ... upload_manager_browser.contents, "log-content") + >>> for log in logs: + ... print(extract_text(log)) + Rejected...a moment ago...by Sample Person...Foo + Accepted...a moment ago...by Sample Person... + Clean up ======== diff --git a/lib/lp/soyuz/templates/distroseries-queue.pt b/lib/lp/soyuz/templates/distroseries-queue.pt index 59b77d5..c210a0b 100644 --- a/lib/lp/soyuz/templates/distroseries-queue.pt +++ b/lib/lp/soyuz/templates/distroseries-queue.pt @@ -149,6 +149,18 @@ </tbody> <tbody tal:attributes="class string:${filelist_class}"> <metal:filelist use-macro="template/macros/package-filelist"/> + <tr class="log-content" tal:repeat="log packageupload/logs"> + <td colspan="2" style="border: 0"></td> + <td colspan="8" style="border: 0"> + <span tal:content="log/new_status"></span> + <span tal:attributes="title log/date_created/fmt:datetime" + tal:content="log/date_created/fmt:displaydate" /> + <span tal:condition="log/person"> + by <span tal:content="structure log/person/fmt:link" /> + </span> + <p tal:condition="log/comment" tal:content="log/comment" /> + </td> + </tr> </tbody> </tal:block> </tal:batch> diff --git a/lib/lp/soyuz/tests/test_packageupload.py b/lib/lp/soyuz/tests/test_packageupload.py index 5f78fe2..c74ad9b 100644 --- a/lib/lp/soyuz/tests/test_packageupload.py +++ b/lib/lp/soyuz/tests/test_packageupload.py @@ -1,4 +1,4 @@ -# Copyright 2009-2018 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). """Test Build features.""" @@ -14,7 +14,10 @@ from lazr.restfulclient.errors import ( BadRequest, Unauthorized, ) -from testtools.matchers import Equals +from testtools.matchers import ( + Equals, + MatchesStructure, + ) import transaction from zope.component import ( getUtility, @@ -84,6 +87,20 @@ class PackageUploadTestCase(TestCaseWithFactory): super(PackageUploadTestCase, self).setUp() self.test_publisher = SoyuzTestPublisher() + def test_add_log_entry(self): + upload = self.factory.makePackageUpload( + status=PackageUploadStatus.UNAPPROVED) + upload = removeSecurityProxy(upload) + self.assertEqual(upload.logs.count(), 0) + + person = self.factory.makePerson() + + upload._addLog(person, PackageUploadStatus.REJECTED, 'just because') + + self.assertThat(upload.logs.one(), MatchesStructure.byEquality( + person=person, old_status=PackageUploadStatus.UNAPPROVED, + new_status=PackageUploadStatus.REJECTED, comment='just because')) + def test_realiseUpload_for_overridden_component_archive(self): # If the component of an upload is overridden to 'Partner' for # example, then the new publishing record should be for the @@ -353,6 +370,12 @@ class PackageUploadTestCase(TestCaseWithFactory): # it goes straight to DONE.) upload_one.acceptFromQueue() self.assertEqual("DONE", upload_one.status.name) + + log = upload_one.logs.one() + self.assertThat(log, MatchesStructure.byEquality( + person=None, old_status=PackageUploadStatus.UNAPPROVED, + new_status=PackageUploadStatus.ACCEPTED, comment=None + )) transaction.commit() # Trying to accept the second fails. @@ -361,9 +384,17 @@ class PackageUploadTestCase(TestCaseWithFactory): self.assertEqual("UNAPPROVED", upload_two.status.name) # Rejecting the second upload works. - upload_two.rejectFromQueue(self.factory.makePerson()) + person = self.factory.makePerson() + upload_two.rejectFromQueue(person, 'Because yes') self.assertEqual("REJECTED", upload_two.status.name) + self.assertEqual(upload_two.logs.count(), 2) + log = upload_two.logs.order_by('id')[1] + self.assertThat(log, MatchesStructure.byEquality( + person=person, old_status=PackageUploadStatus.UNAPPROVED, + new_status=PackageUploadStatus.REJECTED, comment='Because yes' + )) + def test_rejectFromQueue_source_sends_email(self): # Rejecting a source package sends an email to the uploader. self.test_publisher.prepareBreezyAutotest()
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp