launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19280
[Merge] lp:~cjwatson/launchpad/async-upload-notifications into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/async-upload-notifications into lp:launchpad.
Commit message:
Send queue accept/reject notifications from a job rather than directly from the webapp.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #566339 in Launchpad itself: "Cannot accept package which would notify private email addresses"
https://bugs.launchpad.net/launchpad/+bug/566339
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/async-upload-notifications/+merge/270078
Send queue accept/reject notifications from a job rather than directly from the webapp.
Like https://code.launchpad.net/~cjwatson/launchpad/async-branch-notifications/+merge/269788, but for uploads. Most upload notifications already run from scripts or jobs, but acceptFromQueue/rejectFromQueue were exceptions to this.
The resurrected ArchiveJob is arguably a slightly weird choice as we don't use the archive context, but it's not terribly inappropriate for general Soyuz jobs and it saves bothering with a new table.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/async-upload-notifications into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2015-08-26 16:07:10 +0000
+++ database/schema/security.cfg 2015-09-03 15:33:27 +0000
@@ -1332,6 +1332,7 @@
public.answercontact = SELECT
public.archive = SELECT, INSERT, UPDATE
public.archivearch = SELECT, INSERT, UPDATE
+public.archivejob = SELECT, INSERT
public.archivepermission = SELECT
public.binarypackagebuild = SELECT, INSERT, UPDATE
public.binarypackagefile = SELECT, INSERT
@@ -1444,6 +1445,7 @@
public.answercontact = SELECT
public.archive = SELECT, UPDATE
public.archivearch = SELECT, UPDATE
+public.archivejob = SELECT, INSERT, UPDATE
public.archivepermission = SELECT
public.binarypackagebuild = SELECT, INSERT, UPDATE
public.binarypackagefile = SELECT, UPDATE
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2015-09-02 16:50:04 +0000
+++ lib/lp/services/config/schema-lazr.conf 2015-09-03 15:33:27 +0000
@@ -1745,6 +1745,7 @@
ICommercialExpiredJobSource,
IGitRepositoryModifiedMailJobSource,
IMembershipNotificationJobSource,
+ IPackageUploadNotificationJobSource,
IPersonDeactivateJobSource,
IPersonMergeJobSource,
IPlainPackageCopyJobSource,
@@ -1813,6 +1814,11 @@
module: lp.soyuz.interfaces.packagetranslationsuploadjob
dbuser: upload_package_translations_job
+[IPackageUploadNotificationJobSource]
+module: lp.soyuz.interfaces.archivejob
+dbuser: queued
+crontab_group: FREQUENT
+
[IPersonMergeJobSource]
module: lp.registry.interfaces.persontransferjob
dbuser: person-merge-job
=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml 2015-05-19 02:45:25 +0000
+++ lib/lp/soyuz/configure.zcml 2015-09-03 15:33:27 +0000
@@ -417,6 +417,26 @@
interface="lp.soyuz.interfaces.archive.IArchiveSet"/>
</securedutility>
+ <!-- ArchiveJob -->
+
+ <class class="lp.soyuz.model.archivejob.ArchiveJob">
+ <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJob"/>
+ </class>
+ <class class="lp.soyuz.model.archivejob.PackageUploadNotificationJob">
+ <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJob"/>
+ <allow interface="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJob"/>
+ </class>
+ <securedutility
+ component="lp.soyuz.model.archivejob.ArchiveJob"
+ provides="lp.soyuz.interfaces.archivejob.IArchiveJobSource">
+ <allow interface="lp.soyuz.interfaces.archivejob.IArchiveJobSource"/>
+ </securedutility>
+ <securedutility
+ component="lp.soyuz.model.archivejob.PackageUploadNotificationJob"
+ provides="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJobSource">
+ <allow interface="lp.soyuz.interfaces.archivejob.IPackageUploadNotificationJobSource"/>
+ </securedutility>
+
<!-- ArchivePermission -->
<class
=== modified file 'lib/lp/soyuz/doc/distroseriesqueue.txt'
--- lib/lp/soyuz/doc/distroseriesqueue.txt 2015-06-08 00:51:03 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue.txt 2015-09-03 15:33:27 +0000
@@ -821,6 +821,20 @@
Two emails are generated. We won't look what is inside them here, that is
well shown in nascentupload-announcements.txt.
+ >>> from lp.services.config import config
+ >>> from lp.services.job.runner import JobRunner
+ >>> from lp.soyuz.interfaces.archivejob import (
+ ... IPackageUploadNotificationJobSource,
+ ... )
+ >>> from lp.testing.dbuser import dbuser
+
+ >>> def run_package_upload_notification_jobs():
+ ... job_source = getUtility(IPackageUploadNotificationJobSource)
+ ... logger = DevNullLogger()
+ ... with dbuser(config.IPackageUploadNotificationJobSource.dbuser):
+ ... JobRunner.fromReady(job_source, logger).runAll()
+
+ >>> run_package_upload_notification_jobs()
>>> [notification, announcement] = pop_notifications()
When accepting single sources we also immediately create its
@@ -845,6 +859,7 @@
One email is generated (see nascentupload-announcements.txt)
+ >>> run_package_upload_notification_jobs()
>>> [notification] = pop_notifications()
Clean up the librarian files:
=== modified file 'lib/lp/soyuz/enums.py'
--- lib/lp/soyuz/enums.py 2013-07-02 04:02:09 +0000
+++ lib/lp/soyuz/enums.py 2015-09-03 15:33:27 +0000
@@ -1,10 +1,11 @@
-# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Enumerations used in the lp/soyuz modules."""
__metaclass__ = type
__all__ = [
+ 'ArchiveJobType',
'ArchivePermissionType',
'ArchivePurpose',
'ArchiveStatus',
@@ -39,6 +40,19 @@
re_bug_numbers = re.compile(r"\#?\s?(\d+)")
+class ArchiveJobType(DBEnumeratedType):
+ """Values that IArchiveJob.job_type can take."""
+
+ # 0 was once used for COPY_ARCHIVE.
+
+ PACKAGE_UPLOAD_NOTIFICATION = DBItem(1, """
+ Package upload notification
+
+ Send notifications about a package upload being accepted, rejected,
+ or held for approval.
+ """)
+
+
class ArchivePermissionType(DBEnumeratedType):
"""Archive Permission Type.
=== added file 'lib/lp/soyuz/interfaces/archivejob.py'
--- lib/lp/soyuz/interfaces/archivejob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/interfaces/archivejob.py 2015-09-03 15:33:27 +0000
@@ -0,0 +1,66 @@
+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""ArchiveJob interfaces."""
+
+__metaclass__ = type
+
+__all__ = [
+ 'IArchiveJob',
+ 'IArchiveJobSource',
+ 'IPackageUploadNotificationJob',
+ 'IPackageUploadNotificationJobSource',
+ ]
+
+
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
+from zope.schema import (
+ Int,
+ Object,
+ )
+
+from lp import _
+from lp.services.job.interfaces.job import (
+ IJob,
+ IJobSource,
+ IRunnableJob,
+ )
+from lp.soyuz.interfaces.archive import IArchive
+
+
+class IArchiveJob(Interface):
+ """A Job related to an Archive."""
+
+ id = Int(
+ title=_('DB ID'), required=True, readonly=True,
+ description=_("The tracking number for this job."))
+
+ archive = Object(
+ title=_('The archive this job is about.'), schema=IArchive,
+ required=True)
+
+ job = Object(
+ title=_('The common Job attributes'), schema=IJob, required=True)
+
+ metadata = Attribute('A dict of data about the job.')
+
+ def destroySelf():
+ """Destroy this object."""
+
+
+class IArchiveJobSource(IJobSource):
+ """An interface for acquiring IArchiveJobs."""
+
+ def create(archive):
+ """Create a new IArchiveJob for an archive."""
+
+
+class IPackageUploadNotificationJob(IRunnableJob):
+ """A Job to send package upload notifications."""
+
+
+class IPackageUploadNotificationJobSource(IArchiveJobSource):
+ """Interface for acquiring PackageUploadNotificationJobs."""
=== modified file 'lib/lp/soyuz/interfaces/queue.py'
--- lib/lp/soyuz/interfaces/queue.py 2015-09-02 12:05:15 +0000
+++ lib/lp/soyuz/interfaces/queue.py 2015-09-03 15:33:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Queue interfaces."""
@@ -400,12 +400,16 @@
committed to have some updates actually written to the database.
"""
- def notify(summary_text=None, changes_file_object=None, logger=None):
+ def notify(status=None, summary_text=None, changes_file_object=None,
+ logger=None):
"""Notify by email when there is a new distroseriesqueue entry.
This will send new, accept, announce and rejection messages as
appropriate.
+ :param status: The current `PackageUploadStatus` when this
+ notification was generated. Defaults to `self.status`.
+
:param summary_text: Any additional text to append to the auto-
generated summary. This is also the only text used if there is
a rejection message generated.
=== added file 'lib/lp/soyuz/model/archivejob.py'
--- lib/lp/soyuz/model/archivejob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/model/archivejob.py 2015-09-03 15:33:27 +0000
@@ -0,0 +1,178 @@
+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import json
+import logging
+import StringIO
+
+from lazr.delegates import delegate_to
+from storm.expr import And
+from storm.locals import (
+ Int,
+ Reference,
+ Unicode,
+ )
+from zope.component import getUtility
+from zope.interface import (
+ implementer,
+ provider,
+ )
+
+from lp.services.config import config
+from lp.services.database.enumcol import EnumCol
+from lp.services.database.interfaces import IMasterStore
+from lp.services.database.stormbase import StormBase
+from lp.services.job.model.job import (
+ EnumeratedSubclass,
+ Job,
+ )
+from lp.services.job.runner import BaseRunnableJob
+from lp.soyuz.enums import (
+ ArchiveJobType,
+ PackageUploadStatus,
+ )
+from lp.soyuz.interfaces.archivejob import (
+ IArchiveJob,
+ IArchiveJobSource,
+ IPackageUploadNotificationJob,
+ IPackageUploadNotificationJobSource,
+ )
+from lp.soyuz.interfaces.queue import IPackageUploadSet
+from lp.soyuz.model.archive import Archive
+
+
+@implementer(IArchiveJob)
+class ArchiveJob(StormBase):
+ """Base class for jobs related to Archives."""
+
+ __storm_table__ = 'ArchiveJob'
+
+ id = Int(primary=True)
+
+ job_id = Int(name='job')
+ job = Reference(job_id, Job.id)
+
+ archive_id = Int(name='archive')
+ archive = Reference(archive_id, Archive.id)
+
+ job_type = EnumCol(enum=ArchiveJobType, notNull=True)
+
+ _json_data = Unicode('json_data')
+
+ @property
+ def metadata(self):
+ return json.loads(self._json_data)
+
+ def __init__(self, archive, job_type, metadata):
+ """Create an ArchiveJob.
+
+ :param archive: the `IArchive` this job relates to.
+ :param job_type: the `ArchiveJobType` of this job.
+ :param metadata: the type-specific variables, as a json-compatible
+ dict.
+ """
+ super(ArchiveJob, self).__init__()
+ self.job = Job()
+ self.archive = archive
+ self.job_type = job_type
+ self._json_data = unicode(json.dumps(metadata, ensure_ascii=False))
+
+
+@delegate_to(IArchiveJob)
+@provider(IArchiveJobSource)
+class ArchiveJobDerived(BaseRunnableJob):
+ """Intermediate class for deriving from ArchiveJob."""
+
+ __metaclass__ = EnumeratedSubclass
+
+ def __init__(self, job):
+ self.context = job
+
+ @classmethod
+ def create(cls, archive, metadata=None):
+ """See `IArchiveJob`."""
+ if metadata is None:
+ metadata = {}
+ job = ArchiveJob(archive, cls.class_job_type, metadata)
+ derived = cls(job)
+ derived.celeryRunOnCommit()
+ return derived
+
+ @classmethod
+ def iterReady(cls):
+ """Iterate through all ready ArchiveJobs."""
+ store = IMasterStore(ArchiveJob)
+ jobs = store.find(
+ ArchiveJob,
+ And(ArchiveJob.job_type == cls.class_job_type,
+ ArchiveJob.job_id.is_in(Job.ready_jobs)))
+ return (cls(job) for job in jobs)
+
+ def getOopsVars(self):
+ """See `IRunnableJob`."""
+ vars = super(ArchiveJobDerived, self).getOopsVars()
+ vars.extend([
+ ('archive_id', self.context.archive.id),
+ ('archive_job_id', self.context.id),
+ ('archive_job_type', self.context.job_type.title),
+ ])
+ return vars
+
+
+@implementer(IPackageUploadNotificationJob)
+@provider(IPackageUploadNotificationJobSource)
+class PackageUploadNotificationJob(ArchiveJobDerived):
+
+ class_job_type = ArchiveJobType.PACKAGE_UPLOAD_NOTIFICATION
+
+ config = config.IPackageUploadNotificationJobSource
+
+ @classmethod
+ def create(cls, packageupload, summary_text=None):
+ """See `IPackageUploadNotificationJobSource`."""
+ metadata = {
+ 'packageupload_id': packageupload.id,
+ 'packageupload_status': packageupload.status.title,
+ 'summary_text': summary_text,
+ }
+ return super(PackageUploadNotificationJob, cls).create(
+ packageupload.archive, metadata)
+
+ def getOopsVars(self):
+ """See `ArchiveJobDerived`."""
+ vars = super(PackageUploadNotificationJob, self).getOopsVars()
+ vars.extend([
+ ('packageupload_id', self.metadata['packageupload_id']),
+ ('packageupload_status', self.metadata['packageupload_status']),
+ ('summary_text', self.metadata['summary_text']),
+ ])
+ return vars
+
+ @property
+ def packageupload(self):
+ return getUtility(IPackageUploadSet).get(
+ self.metadata['packageupload_id'])
+
+ @property
+ def packageupload_status(self):
+ return PackageUploadStatus.getTermByToken(
+ self.metadata['packageupload_status']).value
+
+ @property
+ def summary_text(self):
+ return self.metadata['summary_text']
+
+ def run(self):
+ """See `IRunnableJob`."""
+ packageupload = self.packageupload
+ if packageupload.changesfile is None:
+ changes_file_object = None
+ else:
+ changes_file_object = StringIO.StringIO(
+ packageupload.changesfile.read())
+ logger = logging.getLogger()
+ packageupload.notify(
+ status=self.packageupload_status, summary_text=self.summary_text,
+ changes_file_object=changes_file_object, logger=logger)
=== modified file 'lib/lp/soyuz/model/queue.py'
--- lib/lp/soyuz/model/queue.py 2015-09-02 12:05:15 +0000
+++ lib/lp/soyuz/model/queue.py 2015-09-03 15:33:27 +0000
@@ -14,7 +14,6 @@
from itertools import chain
import os
import shutil
-import StringIO
import tempfile
from sqlobject import (
@@ -91,6 +90,7 @@
PriorityNotFound,
SectionNotFound,
)
+from lp.soyuz.interfaces.archivejob import IPackageUploadNotificationJobSource
from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
from lp.soyuz.interfaces.component import IComponentSet
from lp.soyuz.interfaces.packagecopyjob import IPackageCopyJobSource
@@ -546,11 +546,7 @@
self.setAccepted()
- changes_file_object = StringIO.StringIO(self.changesfile.read())
- # We explicitly allow unsigned uploads here since the .changes file
- # is pulled from the librarian which are stripped of their
- # signature just before being stored.
- self.notify(changes_file_object=changes_file_object)
+ getUtility(IPackageUploadNotificationJobSource).create(self)
self.syncUpdate()
# If this is a single source upload we can create the
@@ -595,18 +591,12 @@
# don't think we need them for sync rejections.
return
- if self.changesfile is None:
- changes_file_object = None
- else:
- changes_file_object = StringIO.StringIO(self.changesfile.read())
if comment:
summary_text = "Rejected by %s: %s" % (user.displayname, comment)
else:
summary_text = "Rejected by %s." % user.displayname
- # We allow unsigned uploads since they come from the librarian,
- # which are now stored unsigned.
- self.notify(
- changes_file_object=changes_file_object, summary_text=summary_text)
+ getUtility(IPackageUploadNotificationJobSource).create(
+ self, summary_text=summary_text)
self.syncUpdate()
if bool(getFeatureFlag('auditor.enabled')):
client = AuditorClient()
@@ -896,8 +886,11 @@
else:
return None
- def notify(self, summary_text=None, changes_file_object=None, logger=None):
+ def notify(self, status=None, summary_text=None, changes_file_object=None,
+ logger=None):
"""See `IPackageUpload`."""
+ if status is None:
+ status = self.status
status_action = {
PackageUploadStatus.NEW: 'new',
PackageUploadStatus.UNAPPROVED: 'unapproved',
@@ -912,7 +905,7 @@
changesfile_content = 'No changes file content available.'
blamee = self.findPersonToNotify()
mailer = PackageUploadMailer.forAction(
- status_action[self.status], blamee, self.sourcepackagerelease,
+ status_action[status], blamee, self.sourcepackagerelease,
self.builds, self.customfiles, self.archive, self.distroseries,
self.pocket, summary_text=summary_text, changes=changes,
changesfile_content=changesfile_content,
=== modified file 'lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt'
--- lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt 2015-08-26 13:41:21 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt 2015-09-03 15:33:27 +0000
@@ -304,8 +304,21 @@
Swallow any email generated at the upload:
+ >>> from lp.services.config import config
+ >>> from lp.services.job.runner import JobRunner
>>> from lp.services.mail import stub
+ >>> from lp.soyuz.interfaces.archivejob import (
+ ... IPackageUploadNotificationJobSource,
+ ... )
>>> from lp.testing.mail_helpers import pop_notifications
+
+ >>> def run_package_upload_notification_jobs():
+ ... with permissive_security_policy(
+ ... config.IPackageUploadNotificationJobSource.dbuser):
+ ... job_source = getUtility(IPackageUploadNotificationJobSource)
+ ... JobRunner.fromReady(job_source).runAll()
+
+ >>> run_package_upload_notification_jobs()
>>> swallow = pop_notifications()
Set up a second browser on the same page to simulate accidentally posting to
@@ -347,6 +360,7 @@
if it is someone other than the uploader) and (usually) an email to the
distroseries' announcement list (see nascentupload-announcements.txt).
+ >>> run_package_upload_notification_jobs()
>>> [changer_notification, signer_notification,
... announcement] = pop_notifications()
>>> print changer_notification['To']
@@ -368,6 +382,7 @@
No emails are sent in this case:
+ >>> run_package_upload_notification_jobs()
>>> len(stub.test_emails)
0
@@ -513,6 +528,7 @@
Rejecting 'alsa-utils' source:
+ >>> run_package_upload_notification_jobs()
>>> stub.test_emails = []
>>> upload_manager_browser.getControl(name="QUEUE_ID").value = ['4']
@@ -531,6 +547,7 @@
One rejection email is generated:
+ >>> run_package_upload_notification_jobs()
>>> [rejection] = pop_notifications()
>>> rejection['Subject']
'[ubuntu/breezy-autotest] alsa-utils 1.0.9a-4ubuntu1 (Rejected)'
=== added file 'lib/lp/soyuz/tests/test_archivejob.py'
--- lib/lp/soyuz/tests/test_archivejob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/tests/test_archivejob.py 2015-09-03 15:33:27 +0000
@@ -0,0 +1,113 @@
+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from lp.services.job.runner import JobRunner
+from lp.services.mail.sendmail import format_address_for_person
+from lp.soyuz.enums import (
+ ArchiveJobType,
+ PackageUploadStatus,
+ )
+from lp.soyuz.model.archivejob import (
+ ArchiveJob,
+ ArchiveJobDerived,
+ PackageUploadNotificationJob,
+ )
+from lp.testing import TestCaseWithFactory
+from lp.testing.dbuser import dbuser
+from lp.testing.layers import (
+ LaunchpadZopelessLayer,
+ ZopelessDatabaseLayer,
+ )
+from lp.testing.mail_helpers import pop_notifications
+
+
+class TestArchiveJob(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_instantiate(self):
+ # ArchiveJob.__init__() instantiates a ArchiveJob instance.
+ archive = self.factory.makeArchive()
+
+ metadata = ('some', 'arbitrary', 'metadata')
+ archive_job = ArchiveJob(
+ archive, ArchiveJobType.PACKAGE_UPLOAD_NOTIFICATION, metadata)
+
+ self.assertEqual(archive, archive_job.archive)
+ self.assertEqual(
+ ArchiveJobType.PACKAGE_UPLOAD_NOTIFICATION, archive_job.job_type)
+
+ # When we actually access the ArchiveJob's metadata it gets
+ # deserialized from JSON, so the representation returned by
+ # archive_job.metadata will be different from what we originally
+ # passed in.
+ metadata_expected = [u'some', u'arbitrary', u'metadata']
+ self.assertEqual(metadata_expected, archive_job.metadata)
+
+
+class TestArchiveJobDerived(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_create_explodes(self):
+ # ArchiveJobDerived.create() will blow up because it needs to be
+ # subclassed to work properly.
+ archive = self.factory.makeArchive()
+ self.assertRaises(
+ AttributeError, ArchiveJobDerived.create, archive)
+
+
+class TestPackageUploadNotificationJob(TestCaseWithFactory):
+
+ layer = LaunchpadZopelessLayer
+
+ def test_getOopsVars(self):
+ upload = self.factory.makePackageUpload(
+ status=PackageUploadStatus.ACCEPTED)
+ job = PackageUploadNotificationJob.create(
+ upload, summary_text='Fake summary')
+ expected = [
+ ('job_id', job.context.job.id),
+ ('archive_id', upload.archive.id),
+ ('archive_job_id', job.context.id),
+ ('archive_job_type', 'Package upload notification'),
+ ('packageupload_id', upload.id),
+ ('packageupload_status', 'Accepted'),
+ ('summary_text', 'Fake summary'),
+ ]
+ self.assertEqual(expected, job.getOopsVars())
+
+ def test_metadata(self):
+ upload = self.factory.makePackageUpload(
+ status=PackageUploadStatus.ACCEPTED)
+ job = PackageUploadNotificationJob.create(
+ upload, summary_text='Fake summary')
+ expected = {
+ 'packageupload_id': upload.id,
+ 'packageupload_status': 'Accepted',
+ 'summary_text': 'Fake summary',
+ }
+ self.assertEqual(expected, job.metadata)
+ self.assertEqual(upload, job.packageupload)
+ self.assertEqual(
+ PackageUploadStatus.ACCEPTED, job.packageupload_status)
+ self.assertEqual('Fake summary', job.summary_text)
+
+ def test_run(self):
+ # Running a job produces a notification. Detailed tests of which
+ # notifications go to whom live in the PackageUpload and
+ # PackageUploadMailer tests.
+ upload = self.factory.makeSourcePackageUpload()
+ self.factory.makeComponentSelection(
+ upload.distroseries, upload.sourcepackagerelease.component)
+ upload.setAccepted()
+ job = PackageUploadNotificationJob.create(
+ upload, summary_text='Fake summary')
+ with dbuser(job.config.dbuser):
+ JobRunner([job]).runAll()
+ [email] = pop_notifications()
+ self.assertEqual(
+ format_address_for_person(upload.sourcepackagerelease.creator),
+ email['To'])
+ self.assertIn('(Accepted)', email['Subject'])
+ self.assertIn('Fake summary', email.get_payload()[0].get_payload())
=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
--- lib/lp/soyuz/tests/test_packagecopyjob.py 2015-08-25 14:05:24 +0000
+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2015-09-03 15:33:27 +0000
@@ -1716,13 +1716,20 @@
layer = CeleryJobLayer
+ def setUp(self):
+ super(TestViaCelery, self).setUp()
+ # Turn on Celery handling of PCJs and the resulting notification jobs.
+ self.useFixture(FeatureFixture({
+ 'jobs.celery.enabled_classes':
+ 'PlainPackageCopyJob PackageUploadNotificationJob',
+ }))
+
+ def tearDown(self):
+ super(TestViaCelery, self).tearDown()
+ pop_remote_notifications()
+
def test_run(self):
# A proper test run synchronizes packages.
- # Turn on Celery handling of PCJs.
- self.useFixture(FeatureFixture({
- 'jobs.celery.enabled_classes': 'PlainPackageCopyJob',
- }))
-
job = create_proper_job(self.factory)
self.assertEqual("libc", job.package_name)
self.assertEqual("2.8-1", job.package_version)
@@ -1743,10 +1750,6 @@
def test_resume_from_queue(self):
# Accepting a suspended copy from the queue sends it back
# through celery.
- self.useFixture(FeatureFixture({
- 'jobs.celery.enabled_classes': 'PlainPackageCopyJob',
- }))
-
source_pub = self.factory.makeSourcePackagePublishingHistory(
component=u"main", status=PackagePublishingStatus.PUBLISHED)
target_series = self.factory.makeDistroSeries()
=== modified file 'lib/lp/soyuz/tests/test_packageupload.py'
--- lib/lp/soyuz/tests/test_packageupload.py 2015-08-26 13:41:21 +0000
+++ lib/lp/soyuz/tests/test_packageupload.py 2015-09-03 15:33:27 +0000
@@ -27,6 +27,7 @@
from lp.services.config import config
from lp.services.database.interfaces import IStore
from lp.services.job.interfaces.job import JobStatus
+from lp.services.job.runner import JobRunner
from lp.services.librarian.browser import ProxiedLibraryFileAlias
from lp.services.mail import stub
from lp.soyuz.adapters.overrides import SourceOverride
@@ -35,6 +36,7 @@
PackageUploadCustomFormat,
PackageUploadStatus,
)
+from lp.soyuz.interfaces.archivejob import IPackageUploadNotificationJobSource
from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
from lp.soyuz.interfaces.component import IComponentSet
from lp.soyuz.interfaces.queue import (
@@ -54,6 +56,7 @@
StormStatementRecorder,
TestCaseWithFactory,
)
+from lp.testing.dbuser import dbuser
from lp.testing.layers import (
LaunchpadFunctionalLayer,
LaunchpadZopelessLayer,
@@ -182,6 +185,13 @@
transaction.commit()
return upload, uploader
+ def runPackageUploadNotificationJob(self):
+ """Expect one package upload notification job, and run it."""
+ job_source = getUtility(IPackageUploadNotificationJobSource)
+ [job] = list(job_source.iterReady())
+ with dbuser(config.IPackageUploadNotificationJobSource.dbuser):
+ JobRunner([job]).runAll()
+
def assertEmail(self, expected_to_addrs):
"""Pop an email from the stub queue and check its recipients."""
_, to_addrs, _ = stub.test_emails.pop()
@@ -193,6 +203,7 @@
self.test_publisher.prepareBreezyAutotest()
upload, uploader = self.makeSourcePackageUpload()
upload.acceptFromQueue()
+ self.runPackageUploadNotificationJob()
self.assertEqual(2, len(stub.test_emails))
# Emails sent are the uploader's notification and the announcement:
self.assertEmail([uploader.preferredemail.email])
@@ -207,6 +218,7 @@
upload, uploader = self.makeSourcePackageUpload(
pocket=PackagePublishingPocket.BACKPORTS)
upload.acceptFromQueue()
+ self.runPackageUploadNotificationJob()
self.assertEqual(1, len(stub.test_emails))
# Only one email is sent, to the person in the changed-by field. No
# announcement email is sent.
@@ -221,6 +233,7 @@
pocket=PackagePublishingPocket.PROPOSED,
section_name="translations")
upload.acceptFromQueue()
+ self.runPackageUploadNotificationJob()
self.assertEqual("DONE", upload.status.name)
self.assertEqual(0, len(stub.test_emails))
@@ -267,6 +280,7 @@
self.test_publisher.prepareBreezyAutotest()
upload, _ = self.makeBuildPackageUpload()
upload.acceptFromQueue()
+ self.runPackageUploadNotificationJob()
self.assertEqual(0, len(stub.test_emails))
def test_acceptFromQueue_handles_duplicates(self):
@@ -314,6 +328,7 @@
self.test_publisher.prepareBreezyAutotest()
upload, uploader = self.makeSourcePackageUpload()
upload.rejectFromQueue(self.factory.makePerson())
+ self.runPackageUploadNotificationJob()
self.assertEqual(1, len(stub.test_emails))
self.assertEmail([uploader.preferredemail.email])
@@ -322,6 +337,7 @@
self.test_publisher.prepareBreezyAutotest()
upload, uploader = self.makeBuildPackageUpload()
upload.rejectFromQueue(self.factory.makePerson())
+ self.runPackageUploadNotificationJob()
self.assertEqual(1, len(stub.test_emails))
self.assertEmail([uploader.preferredemail.email])
@@ -333,6 +349,7 @@
pocket=PackagePublishingPocket.PROPOSED,
section_name="translations")
upload.rejectFromQueue(self.factory.makePerson())
+ self.runPackageUploadNotificationJob()
self.assertEqual(0, len(stub.test_emails))
def test_rejectFromQueue_source_with_reason(self):
@@ -342,6 +359,7 @@
upload, uploader = self.makeSourcePackageUpload()
person = self.factory.makePerson()
upload.rejectFromQueue(user=person, comment='Because.')
+ self.runPackageUploadNotificationJob()
self.assertEqual(1, len(stub.test_emails))
self.assertIn(
'Rejected:\nRejected by %s: Because.' % person.displayname,
Follow ups