~launchpad-pqm/launchpad/devel

10482.1.8 by Tim Penhey
Make review requests a job.
1
# Copyright 2009, 2010 Canonical Ltd.  This software is licensed under the
8687.15.22 by Karl Fogel
Add the copyright header block to the remaining .py files.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
3
10427.13.1 by Aaron Bentley
Improve oops messages
4
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
5
"""Job classes related to BranchMergeProposals are in here.
6
7
This includes both jobs for the proposals themselves, or jobs that are
8
creating proposals, or diffs relating to the proposals.
9
"""
10
10427.13.1 by Aaron Bentley
Improve oops messages
11
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
12
__metaclass__ = type
10427.13.1 by Aaron Bentley
Improve oops messages
13
14
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
15
__all__ = [
16
    'BranchMergeProposalJob',
7675.624.75 by Tim Penhey
Logic changed slightly for next_preview_diff_job.
17
    'BranchMergeProposalJobFactory',
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
18
    'BranchMergeProposalJobSource',
7675.624.75 by Tim Penhey
Logic changed slightly for next_preview_diff_job.
19
    'BranchMergeProposalJobType',
10482.1.2 by Tim Penhey
Add the job type to the enum.
20
    'CodeReviewCommentEmailJob',
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
21
    'CreateMergeProposalJob',
11486.4.19 by Aaron Bentley
Create GenerateIncrementalDiff jobs missing incremental diffs on tip change.
22
    'GenerateIncrementalDiffJob',
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
23
    'MergeProposalCreatedJob',
11733.1.1 by Tim Penhey
Rename the MergeProposalCreatedJob to be MergeProposalReviewRequestedEmailJob.
24
    'MergeProposalNeedsReviewEmailJob',
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
25
    'MergeProposalUpdatedEmailJob',
10482.1.8 by Tim Penhey
Make review requests a job.
26
    'ReviewRequestedEmailJob',
10100.1.25 by Jonathan Lange
Fix the last of them
27
    'UpdatePreviewDiffJob',
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
28
    ]
29
9826.11.26 by Aaron Bentley
Provide job running context in a ContextManager.
30
import contextlib
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
31
from datetime import (
32
    datetime,
33
    timedelta,
34
    )
10054.23.6 by Aaron Bentley
Fix test name.
35
from email.utils import parseaddr
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
36
37
from lazr.delegates import delegates
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
38
from lazr.enum import (
39
    DBEnumeratedType,
40
    DBItem,
41
    )
7675.624.49 by Tim Penhey
Not yet passing, but getting close.
42
import pytz
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
43
import simplejson
44
from sqlobject import SQLObjectNotFound
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
45
from storm.expr import (
46
    And,
47
    Desc,
48
    Or,
49
    )
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
50
from storm.info import ClassAlias
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
51
from storm.locals import (
52
    Int,
53
    Reference,
54
    Unicode,
55
    )
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
56
from storm.store import Store
57
from zope.component import getUtility
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
58
from zope.interface import (
59
    classProvides,
60
    implements,
61
    )
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
62
10482.1.31 by Tim Penhey
Updates while on skype with reviewer.
63
from canonical.config import config
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
64
from canonical.database.enumcol import EnumCol
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
65
from canonical.launchpad.database.message import (
66
    MessageJob,
67
    MessageJobAction,
68
    )
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
69
from canonical.launchpad.interfaces.message import IMessageJob
9826.11.21 by Aaron Bentley
Ensure oopses are handled correctly.
70
from canonical.launchpad.webapp import errorlog
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
71
from canonical.launchpad.webapp.interaction import setupInteraction
72
from canonical.launchpad.webapp.interfaces import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
73
    DEFAULT_FLAVOR,
74
    IPlacelessAuthUtility,
75
    IStoreSelector,
76
    MAIN_STORE,
77
    MASTER_FLAVOR,
78
    )
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
79
from lp.code.enums import BranchType
8590.1.3 by Tim Penhey
Fix the imports and naming.
80
from lp.code.interfaces.branchmergeproposal import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
81
    IBranchMergeProposalJob,
82
    IBranchMergeProposalJobSource,
83
    ICodeReviewCommentEmailJob,
84
    ICodeReviewCommentEmailJobSource,
85
    ICreateMergeProposalJob,
86
    ICreateMergeProposalJobSource,
11486.4.4 by Aaron Bentley
Fake merge of job removal.
87
    IGenerateIncrementalDiffJob,
88
    IGenerateIncrementalDiffJobSource,
11733.1.1 by Tim Penhey
Rename the MergeProposalCreatedJob to be MergeProposalReviewRequestedEmailJob.
89
    IMergeProposalNeedsReviewEmailJob,
90
    IMergeProposalNeedsReviewEmailJobSource,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
91
    IMergeProposalUpdatedEmailJob,
92
    IMergeProposalUpdatedEmailJobSource,
93
    IReviewRequestedEmailJob,
94
    IReviewRequestedEmailJobSource,
95
    IUpdatePreviewDiffJob,
96
    IUpdatePreviewDiffJobSource,
9222.1.31 by Aaron Bentley
Add and test script for running preview diff update jobs.
97
    )
11486.4.15 by Aaron Bentley
Get job tests passing.
98
from lp.code.interfaces.revision import IRevisionSet
10482.1.8 by Tim Penhey
Make review requests a job.
99
from lp.code.mail.branch import RecipientReason
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
100
from lp.code.mail.branchmergeproposal import BMPMailer
7675.624.3 by Tim Penhey
Make the code review comment email sent by a job.
101
from lp.code.mail.codereviewcomment import CodeReviewCommentMailer
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
102
from lp.code.model.branchmergeproposal import BranchMergeProposal
11486.4.15 by Aaron Bentley
Get job tests passing.
103
from lp.code.model.diff import PreviewDiff
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
104
from lp.codehosting.vfs import (
105
    get_ro_server,
106
    get_rw_server,
107
    )
10482.1.8 by Tim Penhey
Make review requests a job.
108
from lp.registry.interfaces.person import IPersonSet
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
109
from lp.services.job.interfaces.job import JobStatus
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
110
from lp.services.job.model.job import Job
10137.6.14 by Aaron Bentley
Fix lint errors.
111
from lp.services.job.runner import BaseRunnableJob
7675.624.51 by Tim Penhey
Test passes now.
112
from lp.services.mail.sendmail import format_address_for_person
12243.4.3 by j.c.sackett
Moved lp.services.stormbase to lp.services.database.stormbase
113
from lp.services.database.stormbase import StormBase
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
114
115
116
class BranchMergeProposalJobType(DBEnumeratedType):
117
    """Values that ICodeImportJob.state can take."""
118
11733.1.2 by Tim Penhey
Update the job type enum to make more sense.
119
    MERGE_PROPOSAL_NEEDS_REVIEW = DBItem(0, """
120
        Merge proposal needs review
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
121
11733.1.2 by Tim Penhey
Update the job type enum to make more sense.
122
        This job sends mail to all interested parties about the proposal.
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
123
        """)
124
9222.1.24 by Aaron Bentley
Implement UpdatePreviewDiffJob
125
    UPDATE_PREVIEW_DIFF = DBItem(1, """
126
        Update the preview diff for the BranchMergeProposal.
127
128
        This job generates the preview diff for a BranchMergeProposal.
129
        """)
130
10482.1.2 by Tim Penhey
Add the job type to the enum.
131
    CODE_REVIEW_COMMENT_EMAIL = DBItem(2, """
132
        Send the code review comment to the subscribers.
10482.1.3 by Tim Penhey
Test the running of the job and creation on commenting.
133
134
        This job sends the email to the merge proposal subscribers and
135
        reviewers.
10482.1.2 by Tim Penhey
Add the job type to the enum.
136
        """)
137
10482.1.8 by Tim Penhey
Make review requests a job.
138
    REVIEW_REQUEST_EMAIL = DBItem(3, """
139
        Send the review request email to the requested reviewer.
140
141
        This job sends an email to the requested reviewer, or members of the
142
        requested reviewer team asking them to review the proposal.
143
        """)
144
10482.1.14 by Tim Penhey
Start the job process.
145
    MERGE_PROPOSAL_UPDATED = DBItem(4, """
146
        Merge proposal updated
147
148
        This job sends an email to the subscribers informing them of fields
149
        that have been changed on the merge proposal itself.
150
        """)
151
11486.4.4 by Aaron Bentley
Fake merge of job removal.
152
    GENERATE_INCREMENTAL_DIFF = DBItem(5, """
153
        Generate incremental diff
154
155
        This job generates an incremental diff for a merge proposal.""")
156
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
157
12243.4.2 by j.c.sackett
Updated all uses of storm.base.Storm with lp.services.stormbase.StormBase
158
class BranchMergeProposalJob(StormBase):
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
159
    """Base class for jobs related to branch merge proposals."""
160
161
    implements(IBranchMergeProposalJob)
162
163
    __storm_table__ = 'BranchMergeProposalJob'
164
165
    id = Int(primary=True)
166
167
    jobID = Int('job')
168
    job = Reference(jobID, Job.id)
169
170
    branch_merge_proposalID = Int('branch_merge_proposal', allow_none=False)
171
    branch_merge_proposal = Reference(
172
        branch_merge_proposalID, BranchMergeProposal.id)
173
174
    job_type = EnumCol(enum=BranchMergeProposalJobType, notNull=True)
175
176
    _json_data = Unicode('json_data')
177
178
    @property
179
    def metadata(self):
180
        return simplejson.loads(self._json_data)
181
182
    def __init__(self, branch_merge_proposal, job_type, metadata):
183
        """Constructor.
184
185
        :param branch_merge_proposal: The proposal this job relates to.
186
        :param job_type: The BranchMergeProposalJobType of this job.
187
        :param metadata: The type-specific variables, as a JSON-compatible
188
            dict.
189
        """
12243.4.5 by j.c.sackett
Improved subclassing on branchmergeproposaljob.
190
        super(BranchMergeProposalJob, self).__init__()
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
191
        json_data = simplejson.dumps(metadata)
192
        self.job = Job()
193
        self.branch_merge_proposal = branch_merge_proposal
194
        self.job_type = job_type
195
        # XXX AaronBentley 2009-01-29 bug=322819: This should be a bytestring,
196
        # but the DB representation is unicode.
197
        self._json_data = json_data.decode('utf-8')
198
199
    def sync(self):
200
        store = Store.of(self)
201
        store.flush()
202
        store.autoreload(self)
203
204
    def destroySelf(self):
205
        Store.of(self).remove(self)
206
207
    @classmethod
208
    def selectBy(klass, **kwargs):
209
        """Return selected instances of this class.
210
211
        At least one pair of keyword arguments must be supplied.
212
        foo=bar is interpreted as 'select all instances of
213
        BranchMergeProposalJob whose property "foo" is equal to "bar"'.
214
        """
215
        assert len(kwargs) > 0
216
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
217
        return store.find(klass, **kwargs)
218
219
    @classmethod
220
    def get(klass, key):
221
        """Return the instance of this class whose key is supplied.
222
223
        :raises: SQLObjectNotFound
224
        """
225
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
226
        instance = store.get(klass, key)
227
        if instance is None:
228
            raise SQLObjectNotFound(
229
                'No occurrence of %s has key %s' % (klass.__name__, key))
230
        return instance
231
232
8963.10.6 by Aaron Bentley
Refactor job running in terms of IRunnableJob.
233
class BranchMergeProposalJobDerived(BaseRunnableJob):
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
234
235
    """Intermediate class for deriving from BranchMergeProposalJob."""
236
    delegates(IBranchMergeProposalJob)
237
238
    def __init__(self, job):
239
        self.context = job
240
7675.624.52 by Tim Penhey
Lots of logging and a better producer.
241
    def __repr__(self):
242
        bmp = self.branch_merge_proposal
243
        return '<%(job_type)s job for merge %(merge_id)s on %(branch)s>' % {
244
            'job_type': self.context.job_type.name,
245
            'merge_id': bmp.id,
246
            'branch': bmp.source_branch.unique_name,
247
            }
248
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
249
    @classmethod
9222.1.44 by Aaron Bentley
Updates from review.
250
    def create(cls, bmp):
9222.1.24 by Aaron Bentley
Implement UpdatePreviewDiffJob
251
        """See `IMergeProposalCreationJob`."""
252
        job = BranchMergeProposalJob(
9222.1.44 by Aaron Bentley
Updates from review.
253
            bmp, cls.class_job_type, {})
254
        return cls(job)
9222.1.24 by Aaron Bentley
Implement UpdatePreviewDiffJob
255
256
    @classmethod
9826.11.15 by Aaron Bentley
Start trying to use the ampoule process pool.
257
    def get(cls, job_id):
9963.7.7 by Aaron Bentley
cleanup.
258
        """Get a job by id.
259
260
        :return: the BranchMergeProposalJob with the specified id, as the
261
            current BranchMergeProposalJobDereived subclass.
262
        :raises: SQLObjectNotFound if there is no job with the specified id,
263
            or its job_type does not match the desired subclass.
264
        """
9826.11.15 by Aaron Bentley
Start trying to use the ampoule process pool.
265
        job = BranchMergeProposalJob.get(job_id)
9963.7.2 by Aaron Bentley
Allow BranchMergeProposal jobs to be retrieved by their id.
266
        if job.job_type != cls.class_job_type:
267
            raise SQLObjectNotFound(
268
                'No object found with id %d and type %s' % (job_id,
269
                cls.class_job_type.title))
9826.11.15 by Aaron Bentley
Start trying to use the ampoule process pool.
270
        return cls(job)
271
272
    @classmethod
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
273
    def iterReady(klass):
274
        """Iterate through all ready BranchMergeProposalJobs."""
275
        from lp.code.model.branch import Branch
276
        store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
277
        jobs = store.find(
278
            (BranchMergeProposalJob),
279
            And(BranchMergeProposalJob.job_type == klass.class_job_type,
280
                BranchMergeProposalJob.job == Job.id,
281
                Job.id.is_in(Job.ready_jobs),
9826.2.1 by Michael Hudson
test and fix
282
                BranchMergeProposalJob.branch_merge_proposal
283
                    == BranchMergeProposal.id,
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
284
                BranchMergeProposal.source_branch == Branch.id,
285
                # A proposal isn't considered ready if it has no revisions,
286
                # or if it is hosted but pending a mirror.
287
                Branch.revision_count > 0,
288
                Or(Branch.next_mirror_time == None,
12243.4.9 by j.c.sackett
Lint fixes.
289
                   Branch.branch_type != BranchType.HOSTED)))
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
290
        return (klass(job) for job in jobs)
291
9314.1.1 by Aaron Bentley
Add oops var handling to the Job infrastructure
292
    def getOopsVars(self):
9314.2.3 by Aaron Bentley
Update documentation
293
        """See `IRunnableJob`."""
11486.4.13 by Aaron Bentley
Lint fixes.
294
        vars = BaseRunnableJob.getOopsVars(self)
9314.1.1 by Aaron Bentley
Add oops var handling to the Job infrastructure
295
        bmp = self.context.branch_merge_proposal
296
        vars.extend([
297
            ('branchmergeproposal_job_id', self.context.id),
298
            ('branchmergeproposal_job_type', self.context.job_type.title),
299
            ('source_branch', bmp.source_branch.unique_name),
300
            ('target_branch', bmp.target_branch.unique_name)])
301
        return vars
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
302
7675.624.3 by Tim Penhey
Make the code review comment email sent by a job.
303
11733.1.1 by Tim Penhey
Rename the MergeProposalCreatedJob to be MergeProposalReviewRequestedEmailJob.
304
class MergeProposalNeedsReviewEmailJob(BranchMergeProposalJobDerived):
305
    """See `IMergeProposalNeedsReviewEmailJob`."""
306
307
    implements(IMergeProposalNeedsReviewEmailJob)
308
309
    classProvides(IMergeProposalNeedsReviewEmailJobSource)
10482.1.7 by Tim Penhey
Update the branch merge proposal job source interfaces to inherit from IJobSource.
310
11733.1.2 by Tim Penhey
Update the job type enum to make more sense.
311
    class_job_type = BranchMergeProposalJobType.MERGE_PROPOSAL_NEEDS_REVIEW
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
312
7675.624.21 by Tim Penhey
The merge proposal created job no longer generates the diff.
313
    def run(self):
11733.1.1 by Tim Penhey
Rename the MergeProposalCreatedJob to be MergeProposalReviewRequestedEmailJob.
314
        """See `IMergeProposalNeedsReviewEmailJob`."""
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
315
        mailer = BMPMailer.forCreation(
316
            self.branch_merge_proposal, self.branch_merge_proposal.registrant)
317
        mailer.sendAll()
318
8963.10.3 by Aaron Bentley
Provide notifications for BranchMergeProposal jobs.
319
    def getOopsRecipients(self):
320
        return [self.branch_merge_proposal.registrant.preferredemail.email]
321
322
    def getOperationDescription(self):
323
        return ('notifying people about the proposal to merge %s into %s' %
324
            (self.branch_merge_proposal.source_branch.bzr_identity,
325
             self.branch_merge_proposal.target_branch.bzr_identity))
326
327
7675.624.49 by Tim Penhey
Not yet passing, but getting close.
328
class UpdatePreviewDiffNotReady(Exception):
329
    """Raised if the the preview diff is not ready to run."""
330
331
9222.1.24 by Aaron Bentley
Implement UpdatePreviewDiffJob
332
class UpdatePreviewDiffJob(BranchMergeProposalJobDerived):
9222.1.44 by Aaron Bentley
Updates from review.
333
    """A job to update the preview diff for a branch merge proposal.
334
335
    Provides class methods to create and retrieve such jobs.
336
    """
9222.1.24 by Aaron Bentley
Implement UpdatePreviewDiffJob
337
7675.624.49 by Tim Penhey
Not yet passing, but getting close.
338
    implements(IUpdatePreviewDiffJob)
9222.1.29 by Aaron Bentley
Switch test to JobRunner.
339
9222.1.31 by Aaron Bentley
Add and test script for running preview diff update jobs.
340
    classProvides(IUpdatePreviewDiffJobSource)
341
9222.1.24 by Aaron Bentley
Implement UpdatePreviewDiffJob
342
    class_job_type = BranchMergeProposalJobType.UPDATE_PREVIEW_DIFF
343
7675.624.51 by Tim Penhey
Test passes now.
344
    user_error_types = (UpdatePreviewDiffNotReady, )
345
346
    def checkReady(self):
7675.624.49 by Tim Penhey
Not yet passing, but getting close.
347
        """Is this job ready to run?"""
348
        bmp = self.branch_merge_proposal
349
        if bmp.source_branch.last_scanned_id is None:
350
            raise UpdatePreviewDiffNotReady(
351
                'The source branch has no revisions.')
352
        if bmp.target_branch.last_scanned_id is None:
353
            raise UpdatePreviewDiffNotReady(
354
                'The target branch has no revisions.')
355
        if bmp.source_branch.pending_writes:
356
            raise UpdatePreviewDiffNotReady(
357
                'The source branch has pending writes.')
358
9826.11.20 by Aaron Bentley
Avoid repeating setup and teardown.
359
    @staticmethod
9826.11.26 by Aaron Bentley
Provide job running context in a ContextManager.
360
    @contextlib.contextmanager
361
    def contextManager():
9826.13.9 by Aaron Bentley
Update for review.
362
        """See `IUpdatePreviewDiffJobSource`."""
9826.11.21 by Aaron Bentley
Ensure oopses are handled correctly.
363
        errorlog.globalErrorUtility.configure('update_preview_diffs')
9590.1.112 by Michael Hudson
server fixes, fixes the scanner script tests
364
        server = get_ro_server()
10197.5.13 by Michael Hudson
misc. fixes
365
        server.start_server()
9826.11.26 by Aaron Bentley
Provide job running context in a ContextManager.
366
        yield
10197.5.13 by Michael Hudson
misc. fixes
367
        server.stop_server()
9826.11.20 by Aaron Bentley
Avoid repeating setup and teardown.
368
7675.580.1 by Aaron Bentley
Increase the timeout for UpdatePreviewDiff jobs to 10 minutes.
369
    def acquireLease(self, duration=600):
370
        return self.job.acquireLease(duration)
371
9222.1.24 by Aaron Bentley
Implement UpdatePreviewDiffJob
372
    def run(self):
10482.1.5 by Tim Penhey
Updates from code review comments.
373
        """See `IRunnableJob`."""
7675.624.51 by Tim Penhey
Test passes now.
374
        self.checkReady()
9222.1.24 by Aaron Bentley
Implement UpdatePreviewDiffJob
375
        preview = PreviewDiff.fromBranchMergeProposal(
376
            self.branch_merge_proposal)
377
        self.branch_merge_proposal.preview_diff = preview
378
7675.624.51 by Tim Penhey
Test passes now.
379
    def getOperationDescription(self):
380
        return ('generating the diff for a merge proposal')
381
382
    def getErrorRecipients(self):
383
        """Return a list of email-ids to notify about user errors."""
384
        registrant = self.branch_merge_proposal.registrant
385
        return format_address_for_person(registrant)
386
9222.1.24 by Aaron Bentley
Implement UpdatePreviewDiffJob
387
8963.10.6 by Aaron Bentley
Refactor job running in terms of IRunnableJob.
388
class CreateMergeProposalJob(BaseRunnableJob):
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
389
    """See `ICreateMergeProposalJob` and `ICreateMergeProposalJobSource`."""
390
391
    classProvides(ICreateMergeProposalJobSource)
392
393
    delegates(IMessageJob)
394
395
    class_action = MessageJobAction.CREATE_MERGE_PROPOSAL
396
397
    implements(ICreateMergeProposalJob)
398
399
    def __init__(self, context):
400
        """Create an instance of CreateMergeProposalJob.
401
402
        :param context: a MessageJob.
403
        """
404
        self.context = context
405
406
    def __eq__(self, other):
407
        return (self.__class__ == other.__class__ and
408
                self.context == other.context)
409
410
    @classmethod
411
    def create(klass, message_bytes):
412
        """See `ICreateMergeProposalJobSource`."""
413
        context = MessageJob(
414
            message_bytes, MessageJobAction.CREATE_MERGE_PROPOSAL)
415
        return klass(context)
416
417
    @classmethod
418
    def iterReady(klass):
419
        """Iterate through all ready BranchMergeProposalJobs."""
420
        store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
421
        jobs = store.find(
422
            (MessageJob),
423
            And(MessageJob.action == klass.class_action,
424
                MessageJob.job == Job.id,
425
                Job.id.is_in(Job.ready_jobs)))
426
        return (klass(job) for job in jobs)
427
428
    def run(self):
429
        """See `ICreateMergeProposalJob`."""
430
        # Avoid circular import
431
        from lp.code.mail.codehandler import CodeHandler
10427.13.1 by Aaron Bentley
Improve oops messages
432
        url = self.context.message_bytes.getURL()
433
        with errorlog.globalErrorUtility.oopsMessage('Mail url: %r' % url):
434
            message = self.getMessage()
435
            # Since the message was checked as signed before it was saved in
436
            # the Librarian, just create the principal from the sender and set
437
            # up the interaction.
438
            name, email_addr = parseaddr(message['From'])
439
            authutil = getUtility(IPlacelessAuthUtility)
440
            principal = authutil.getPrincipalByLogin(email_addr)
441
            if principal is None:
442
                raise AssertionError('No principal found for %s' % email_addr)
443
            setupInteraction(principal, email_addr)
8590.1.2 by Tim Penhey
Break out the jobs from the merge proposal module.
444
9590.1.112 by Michael Hudson
server fixes, fixes the scanner script tests
445
            server = get_rw_server()
10427.13.1 by Aaron Bentley
Improve oops messages
446
            server.start_server()
447
            try:
448
                return CodeHandler().processMergeProposal(message)
449
            finally:
450
                server.stop_server()
8963.10.3 by Aaron Bentley
Provide notifications for BranchMergeProposal jobs.
451
452
    def getOopsRecipients(self):
453
        message = self.getMessage()
8963.10.10 by Aaron Bentley
Fix test failures, do more interface inheritance.
454
        from_ = message['From']
455
        if from_ is None:
456
            return []
457
        return [from_]
8963.10.3 by Aaron Bentley
Provide notifications for BranchMergeProposal jobs.
458
459
    def getOperationDescription(self):
460
        message = self.getMessage()
461
        return ('creating a merge proposal from message with subject %s' %
462
                message['Subject'])
7675.624.3 by Tim Penhey
Make the code review comment email sent by a job.
463
464
465
class CodeReviewCommentEmailJob(BranchMergeProposalJobDerived):
466
    """A job to send a code review comment.
467
468
    Provides class methods to create and retrieve such jobs.
469
    """
470
471
    implements(ICodeReviewCommentEmailJob)
10482.1.5 by Tim Penhey
Updates from code review comments.
472
7675.624.3 by Tim Penhey
Make the code review comment email sent by a job.
473
    classProvides(ICodeReviewCommentEmailJobSource)
474
475
    class_job_type = BranchMergeProposalJobType.CODE_REVIEW_COMMENT_EMAIL
476
477
    def run(self):
10482.1.5 by Tim Penhey
Updates from code review comments.
478
        """See `IRunnableJob`."""
479
        mailer = CodeReviewCommentMailer.forCreation(self.code_review_comment)
7675.624.3 by Tim Penhey
Make the code review comment email sent by a job.
480
        mailer.sendAll()
481
482
    @classmethod
483
    def create(cls, code_review_comment):
10482.1.5 by Tim Penhey
Updates from code review comments.
484
        """See `ICodeReviewCommentEmailJobSource`."""
7675.624.3 by Tim Penhey
Make the code review comment email sent by a job.
485
        metadata = cls.getMetadata(code_review_comment)
486
        bmp = code_review_comment.branch_merge_proposal
487
        job = BranchMergeProposalJob(bmp, cls.class_job_type, metadata)
488
        return cls(job)
489
490
    @staticmethod
491
    def getMetadata(code_review_comment):
492
        return {'code_review_comment': code_review_comment.id}
493
10482.1.5 by Tim Penhey
Updates from code review comments.
494
    @property
7675.624.3 by Tim Penhey
Make the code review comment email sent by a job.
495
    def code_review_comment(self):
10482.1.5 by Tim Penhey
Updates from code review comments.
496
        """Get the code review comment."""
10482.1.3 by Tim Penhey
Test the running of the job and creation on commenting.
497
        return self.branch_merge_proposal.getComment(
498
            self.metadata['code_review_comment'])
10482.1.1 by Tim Penhey
Make the code review comment email sent by a job.
499
10482.1.7 by Tim Penhey
Update the branch merge proposal job source interfaces to inherit from IJobSource.
500
    def getOopsVars(self):
501
        """See `IRunnableJob`."""
11486.4.13 by Aaron Bentley
Lint fixes.
502
        vars = BranchMergeProposalJobDerived.getOopsVars(self)
10482.1.7 by Tim Penhey
Update the branch merge proposal job source interfaces to inherit from IJobSource.
503
        vars.extend([
504
            ('code_review_comment', self.metadata['code_review_comment']),
505
            ])
506
        return vars
507
508
    def getErrorRecipients(self):
509
        """Return a list of email-ids to notify about user errors."""
510
        commenter = self.code_review_comment.message.owner
10482.1.22 by Tim Penhey
Format the email address for the error recipients properly.
511
        return [format_address_for_person(commenter)]
10482.1.8 by Tim Penhey
Make review requests a job.
512
7675.659.1 by Tim Penhey
Test and add more operation descriptions.
513
    def getOperationDescription(self):
514
        return 'emailing a code review comment'
515
10482.1.8 by Tim Penhey
Make review requests a job.
516
517
class ReviewRequestedEmailJob(BranchMergeProposalJobDerived):
518
    """Send email to the reviewer telling them to review the proposal.
519
520
    Provides class methods to create and retrieve such jobs.
521
    """
522
523
    implements(IReviewRequestedEmailJob)
524
525
    classProvides(IReviewRequestedEmailJobSource)
526
527
    class_job_type = BranchMergeProposalJobType.REVIEW_REQUEST_EMAIL
528
529
    def run(self):
530
        """See `IRunnableJob`."""
531
        reason = RecipientReason.forReviewer(
532
            self.branch_merge_proposal, True, self.reviewer)
533
        mailer = BMPMailer.forReviewRequest(
534
            reason, self.branch_merge_proposal, self.requester)
535
        mailer.sendAll()
536
537
    @classmethod
538
    def create(cls, review_request):
539
        """See `IReviewRequestedEmailJobSource`."""
540
        metadata = cls.getMetadata(review_request)
541
        bmp = review_request.branch_merge_proposal
542
        job = BranchMergeProposalJob(bmp, cls.class_job_type, metadata)
543
        return cls(job)
544
545
    @staticmethod
546
    def getMetadata(review_request):
547
        return {
548
            'reviewer': review_request.reviewer.name,
549
            'requester': review_request.registrant.name,
550
            }
551
552
    @property
553
    def reviewer(self):
554
        """The person or team who has been asked to review."""
555
        return getUtility(IPersonSet).getByName(self.metadata['reviewer'])
556
557
    @property
558
    def requester(self):
559
        """The person who requested the review to be done."""
560
        return getUtility(IPersonSet).getByName(self.metadata['requester'])
561
562
    def getOopsVars(self):
563
        """See `IRunnableJob`."""
11486.4.13 by Aaron Bentley
Lint fixes.
564
        vars = BranchMergeProposalJobDerived.getOopsVars(self)
10482.1.8 by Tim Penhey
Make review requests a job.
565
        vars.extend([
566
            ('reviewer', self.metadata['reviewer']),
567
            ('requester', self.metadata['requester']),
568
            ])
569
        return vars
570
571
    def getErrorRecipients(self):
572
        """Return a list of email-ids to notify about user errors."""
573
        recipients = []
574
        if self.requester is not None:
7675.624.51 by Tim Penhey
Test passes now.
575
            recipients.append(format_address_for_person(self.requester))
10482.1.8 by Tim Penhey
Make review requests a job.
576
        return recipients
10482.1.14 by Tim Penhey
Start the job process.
577
7675.659.1 by Tim Penhey
Test and add more operation descriptions.
578
    def getOperationDescription(self):
579
        return 'emailing a reviewer requesting a review'
580
10482.1.14 by Tim Penhey
Start the job process.
581
10482.1.16 by Tim Penhey
Register the global utilities.
582
class MergeProposalUpdatedEmailJob(BranchMergeProposalJobDerived):
10482.1.14 by Tim Penhey
Start the job process.
583
    """Send email to the subscribers informing them of updated fields.
584
585
    When attributes of the merge proposal are edited, we inform the
586
    subscribers.
587
    """
588
10482.1.16 by Tim Penhey
Register the global utilities.
589
    implements(IMergeProposalUpdatedEmailJob)
10482.1.14 by Tim Penhey
Start the job process.
590
10482.1.16 by Tim Penhey
Register the global utilities.
591
    classProvides(IMergeProposalUpdatedEmailJobSource)
10482.1.14 by Tim Penhey
Start the job process.
592
593
    class_job_type = BranchMergeProposalJobType.MERGE_PROPOSAL_UPDATED
594
595
    def run(self):
596
        """See `IRunnableJob`."""
10482.1.18 by Tim Penhey
Emails describing merge proposal updates now jobified.
597
        mailer = BMPMailer.forModification(
598
            self.branch_merge_proposal, self.delta_text, self.editor)
599
        mailer.sendAll()
10482.1.14 by Tim Penhey
Start the job process.
600
601
    @classmethod
10482.1.18 by Tim Penhey
Emails describing merge proposal updates now jobified.
602
    def create(cls, merge_proposal, delta_text, editor):
10482.1.14 by Tim Penhey
Start the job process.
603
        """See `IReviewRequestedEmailJobSource`."""
10482.1.18 by Tim Penhey
Emails describing merge proposal updates now jobified.
604
        metadata = cls.getMetadata(delta_text, editor)
605
        job = BranchMergeProposalJob(
606
            merge_proposal, cls.class_job_type, metadata)
10482.1.14 by Tim Penhey
Start the job process.
607
        return cls(job)
608
609
    @staticmethod
10482.1.18 by Tim Penhey
Emails describing merge proposal updates now jobified.
610
    def getMetadata(delta_text, editor):
611
        metadata = {'delta_text': delta_text}
612
        if editor is not None:
11486.4.13 by Aaron Bentley
Lint fixes.
613
            metadata['editor'] = editor.name
10482.1.18 by Tim Penhey
Emails describing merge proposal updates now jobified.
614
        return metadata
615
616
    @property
617
    def editor(self):
618
        """The person who updated the merge proposal."""
619
        editor_name = self.metadata.get('editor')
620
        if editor_name is None:
621
            return None
622
        else:
623
            return getUtility(IPersonSet).getByName(editor_name)
624
625
    @property
626
    def delta_text(self):
627
        """The changes that were made to the merge proposal."""
628
        return self.metadata['delta_text']
10482.1.14 by Tim Penhey
Start the job process.
629
630
    def getOopsVars(self):
631
        """See `IRunnableJob`."""
11486.4.13 by Aaron Bentley
Lint fixes.
632
        vars = BranchMergeProposalJobDerived.getOopsVars(self)
10482.1.14 by Tim Penhey
Start the job process.
633
        vars.extend([
10482.1.18 by Tim Penhey
Emails describing merge proposal updates now jobified.
634
            ('editor', self.metadata.get('editor', '(not set)')),
635
            ('delta_text', self.metadata['delta_text']),
10482.1.14 by Tim Penhey
Start the job process.
636
            ])
637
        return vars
638
639
    def getErrorRecipients(self):
640
        """Return a list of email-ids to notify about user errors."""
641
        recipients = []
10482.1.18 by Tim Penhey
Emails describing merge proposal updates now jobified.
642
        if self.editor is not None:
7675.624.51 by Tim Penhey
Test passes now.
643
            recipients.append(format_address_for_person(self.editor))
10482.1.14 by Tim Penhey
Start the job process.
644
        return recipients
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
645
7675.659.1 by Tim Penhey
Test and add more operation descriptions.
646
    def getOperationDescription(self):
647
        return 'emailing subscribers about merge proposal changes'
648
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
649
11486.4.4 by Aaron Bentley
Fake merge of job removal.
650
class GenerateIncrementalDiffJob(BranchMergeProposalJobDerived):
651
    """A job to generate an incremental diff for a branch merge proposal.
652
653
    Provides class methods to create and retrieve such jobs.
654
    """
655
656
    implements(IGenerateIncrementalDiffJob)
657
658
    classProvides(IGenerateIncrementalDiffJobSource)
659
660
    class_job_type = BranchMergeProposalJobType.GENERATE_INCREMENTAL_DIFF
661
11486.4.15 by Aaron Bentley
Get job tests passing.
662
    def acquireLease(self, duration=600):
663
        return self.job.acquireLease(duration)
664
11486.4.4 by Aaron Bentley
Fake merge of job removal.
665
    def run(self):
11486.4.15 by Aaron Bentley
Get job tests passing.
666
        revision_set = getUtility(IRevisionSet)
667
        old_revision = revision_set.getByRevisionId(self.old_revision_id)
668
        new_revision = revision_set.getByRevisionId(self.new_revision_id)
669
        diff = self.branch_merge_proposal.generateIncrementalDiff(
670
            old_revision, new_revision)
11486.4.4 by Aaron Bentley
Fake merge of job removal.
671
672
    @classmethod
673
    def create(cls, merge_proposal, old_revision_id, new_revision_id):
674
        metadata = cls.getMetadata(old_revision_id, new_revision_id)
675
        job = BranchMergeProposalJob(
676
            merge_proposal, cls.class_job_type, metadata)
677
        return cls(job)
678
679
    @staticmethod
680
    def getMetadata(old_revision_id, new_revision_id):
681
        return {
682
            'old_revision_id': old_revision_id,
683
            'new_revision_id': new_revision_id,
684
        }
685
686
    @property
687
    def old_revision_id(self):
688
        """The old revision id for the diff."""
689
        return self.metadata['old_revision_id']
690
691
    @property
692
    def new_revision_id(self):
693
        """The new revision id for the diff."""
694
        return self.metadata['new_revision_id']
695
696
    def getOopsVars(self):
697
        """See `IRunnableJob`."""
11486.4.13 by Aaron Bentley
Lint fixes.
698
        vars = BranchMergeProposalJobDerived.getOopsVars(self)
11486.4.4 by Aaron Bentley
Fake merge of job removal.
699
        vars.extend([
700
            ('old_revision_id', self.metadata['old_revision_id']),
701
            ('new_revision_id', self.metadata['new_revision_id']),
702
            ])
703
        return vars
704
705
    def getOperationDescription(self):
706
        return ('generating an incremental diff for a merge proposal')
707
708
    def getErrorRecipients(self):
709
        """Return a list of email-ids to notify about user errors."""
710
        registrant = self.branch_merge_proposal.registrant
711
        return format_address_for_person(registrant)
712
713
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
714
class BranchMergeProposalJobFactory:
715
    """Construct a derived merge proposal job for a BranchMergeProposalJob."""
716
717
    job_classes = {
11733.1.2 by Tim Penhey
Update the job type enum to make more sense.
718
        BranchMergeProposalJobType.MERGE_PROPOSAL_NEEDS_REVIEW:
11733.1.1 by Tim Penhey
Rename the MergeProposalCreatedJob to be MergeProposalReviewRequestedEmailJob.
719
            MergeProposalNeedsReviewEmailJob,
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
720
        BranchMergeProposalJobType.UPDATE_PREVIEW_DIFF:
721
            UpdatePreviewDiffJob,
722
        BranchMergeProposalJobType.CODE_REVIEW_COMMENT_EMAIL:
723
            CodeReviewCommentEmailJob,
724
        BranchMergeProposalJobType.REVIEW_REQUEST_EMAIL:
725
            ReviewRequestedEmailJob,
726
        BranchMergeProposalJobType.MERGE_PROPOSAL_UPDATED:
727
            MergeProposalUpdatedEmailJob,
11486.4.4 by Aaron Bentley
Fake merge of job removal.
728
        BranchMergeProposalJobType.GENERATE_INCREMENTAL_DIFF:
729
            GenerateIncrementalDiffJob,
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
730
        }
731
732
    @classmethod
733
    def create(cls, bmp_job):
734
        """Create the derived job for the bmp_job's job type."""
735
        job_class = cls.job_classes[bmp_job.job_type]
736
        return job_class(bmp_job)
737
738
739
class BranchMergeProposalJobSource:
740
    """Provide a job source for all merge proposal jobs.
741
742
    Only one job for any particular merge proposal is returned.
743
    """
744
745
    classProvides(IBranchMergeProposalJobSource)
746
747
    @staticmethod
748
    @contextlib.contextmanager
749
    def contextManager():
750
        """See `IJobSource`."""
751
        errorlog.globalErrorUtility.configure('merge_proposal_jobs')
9590.14.1 by Michael Hudson
merge db-devel via lower pipes, fixing conflicts
752
        server = get_ro_server()
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
753
        server.start_server()
754
        yield
755
        server.stop_server()
756
757
    @staticmethod
758
    def get(job_id):
759
        """Get a job by id.
760
761
        :return: the BranchMergeProposalJob with the specified id, as the
762
            current BranchMergeProposalJobDereived subclass.
763
        :raises: SQLObjectNotFound if there is no job with the specified id,
764
            or its job_type does not match the desired subclass.
765
        """
766
        job = BranchMergeProposalJob.get(job_id)
767
        return BranchMergeProposalJobFactory.create(job)
768
769
    @staticmethod
11486.4.19 by Aaron Bentley
Create GenerateIncrementalDiff jobs missing incremental diffs on tip change.
770
    def iterReady(job_type=None):
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
771
        from lp.code.model.branch import Branch
772
        store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
773
        SourceBranch = ClassAlias(Branch)
774
        TargetBranch = ClassAlias(Branch)
11486.4.36 by Aaron Bentley
Updates from review.
775
        clauses = [
776
            BranchMergeProposalJob.job == Job.id,
777
            Job._status.is_in([JobStatus.WAITING, JobStatus.RUNNING]),
778
            BranchMergeProposalJob.branch_merge_proposal ==
779
            BranchMergeProposal.id, BranchMergeProposal.source_branch ==
780
            SourceBranch.id, BranchMergeProposal.target_branch ==
781
            TargetBranch.id,
782
            ]
11486.4.19 by Aaron Bentley
Create GenerateIncrementalDiff jobs missing incremental diffs on tip change.
783
        if job_type is not None:
784
            clauses.append(BranchMergeProposalJob.job_type == job_type)
785
        jobs = store.find(
786
            (BranchMergeProposalJob, Job, BranchMergeProposal,
787
             SourceBranch, TargetBranch), And(*clauses))
10482.1.31 by Tim Penhey
Updates while on skype with reviewer.
788
        # Order by the job status first (to get running before waiting), then
789
        # the date_created, then job type.  This should give us all creation
790
        # jobs before comment jobs.
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
791
        jobs = jobs.order_by(
7675.624.25 by Tim Penhey
If there is a job running for a merge proposal, don't run any extra jobs.
792
            Desc(Job._status), Job.date_created,
793
            Desc(BranchMergeProposalJob.job_type))
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
794
        # Now only return one job for any given merge proposal.
795
        ready_jobs = []
796
        seen_merge_proposals = set()
797
        for bmp_job, job, bmp, source, target in jobs:
798
            # If we've seen this merge proposal already, skip this job.
799
            if bmp.id in seen_merge_proposals:
800
                continue
7675.624.25 by Tim Penhey
If there is a job running for a merge proposal, don't run any extra jobs.
801
            # We have now seen this merge proposal.
802
            seen_merge_proposals.add(bmp.id)
803
            # If the job is running, then skip it
804
            if job.status == JobStatus.RUNNING:
805
                continue
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
806
            derived_job = BranchMergeProposalJobFactory.create(bmp_job)
7675.624.49 by Tim Penhey
Not yet passing, but getting close.
807
            # If the job is an update preview diff, then check that it is
808
            # ready.
809
            if IUpdatePreviewDiffJob.providedBy(derived_job):
810
                try:
7675.624.51 by Tim Penhey
Test passes now.
811
                    derived_job.checkReady()
7675.624.49 by Tim Penhey
Not yet passing, but getting close.
812
                except UpdatePreviewDiffNotReady:
813
                    # If the job was created under 15 minutes ago wait a bit.
10482.1.31 by Tim Penhey
Updates while on skype with reviewer.
814
                    minutes = (
815
                        config.codehosting.update_preview_diff_ready_timeout)
816
                    cut_off_time = (
817
                        datetime.now(pytz.UTC) - timedelta(minutes=minutes))
7675.624.49 by Tim Penhey
Not yet passing, but getting close.
818
                    if job.date_created > cut_off_time:
819
                        continue
7675.624.18 by Tim Penhey
Make one job source for all merge proposal jobs.
820
            ready_jobs.append(derived_job)
821
        return ready_jobs