~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/code/model/revision.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-12-22 04:55:30 UTC
  • mfrom: (14577.1.1 testfix)
  • Revision ID: launchpad@pqm.canonical.com-20111222045530-wki9iu6c0ysqqwkx
[r=wgrant][no-qa] Fix test_publisherconfig lpstorm import. Probably a
        silent conflict between megalint and apocalypse.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
# pylint: disable-msg=E0611,W0212
 
5
 
 
6
__metaclass__ = type
 
7
__all__ = [
 
8
    'Revision',
 
9
    'RevisionAuthor',
 
10
    'RevisionCache',
 
11
    'RevisionParent',
 
12
    'RevisionProperty',
 
13
    'RevisionSet']
 
14
 
 
15
from datetime import (
 
16
    datetime,
 
17
    timedelta,
 
18
    )
 
19
import email
 
20
 
 
21
from bzrlib.revision import NULL_REVISION
 
22
import pytz
 
23
from sqlobject import (
 
24
    BoolCol,
 
25
    ForeignKey,
 
26
    IntCol,
 
27
    SQLMultipleJoin,
 
28
    SQLObjectNotFound,
 
29
    StringCol,
 
30
    )
 
31
from storm.expr import (
 
32
    And,
 
33
    Asc,
 
34
    Desc,
 
35
    Join,
 
36
    Not,
 
37
    Or,
 
38
    Select,
 
39
    )
 
40
from storm.locals import (
 
41
    Bool,
 
42
    Int,
 
43
    Min,
 
44
    Reference,
 
45
    Storm,
 
46
    )
 
47
from storm.store import Store
 
48
from zope.component import getUtility
 
49
from zope.interface import implements
 
50
from zope.security.proxy import removeSecurityProxy
 
51
 
 
52
from canonical.database.constants import (
 
53
    DEFAULT,
 
54
    UTC_NOW,
 
55
    )
 
56
from canonical.database.datetimecol import UtcDateTimeCol
 
57
from canonical.database.sqlbase import (
 
58
    quote,
 
59
    SQLBase,
 
60
    sqlvalues,
 
61
    )
 
62
from canonical.launchpad.helpers import shortlist
 
63
from lp.services.database.lpstorm import (
 
64
    IMasterStore,
 
65
    IStore,
 
66
    )
 
67
from canonical.launchpad.webapp.interfaces import (
 
68
    DEFAULT_FLAVOR,
 
69
    IStoreSelector,
 
70
    MAIN_STORE,
 
71
    )
 
72
from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
 
73
from lp.code.interfaces.revision import (
 
74
    IRevision,
 
75
    IRevisionAuthor,
 
76
    IRevisionParent,
 
77
    IRevisionProperty,
 
78
    IRevisionSet,
 
79
    )
 
80
from lp.registry.interfaces.person import validate_public_person
 
81
from lp.registry.interfaces.product import IProduct
 
82
from lp.registry.interfaces.projectgroup import IProjectGroup
 
83
from lp.registry.model.person import ValidPersonCache
 
84
from lp.services.identity.interfaces.emailaddress import (
 
85
    EmailAddressStatus,
 
86
    IEmailAddressSet,
 
87
    )
 
88
 
 
89
 
 
90
class Revision(SQLBase):
 
91
    """See IRevision."""
 
92
 
 
93
    implements(IRevision)
 
94
 
 
95
    date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)
 
96
    log_body = StringCol(notNull=True)
 
97
    gpgkey = ForeignKey(dbName='gpgkey', foreignKey='GPGKey', default=None)
 
98
 
 
99
    revision_author_id = Int(name='revision_author', allow_none=False)
 
100
    revision_author = Reference(revision_author_id, 'RevisionAuthor.id')
 
101
 
 
102
    revision_id = StringCol(notNull=True, alternateID=True,
 
103
                            alternateMethodName='byRevisionID')
 
104
    revision_date = UtcDateTimeCol(notNull=False)
 
105
 
 
106
    karma_allocated = BoolCol(default=False, notNull=True)
 
107
 
 
108
    properties = SQLMultipleJoin('RevisionProperty', joinColumn='revision')
 
109
 
 
110
    @property
 
111
    def parents(self):
 
112
        """See IRevision.parents"""
 
113
        return shortlist(RevisionParent.selectBy(
 
114
            revision=self, orderBy='sequence'))
 
115
 
 
116
    @property
 
117
    def parent_ids(self):
 
118
        """Sequence of globally unique ids for the parents of this revision.
 
119
 
 
120
        The corresponding Revision objects can be retrieved, if they are
 
121
        present in the database, using the RevisionSet Zope utility.
 
122
        """
 
123
        return [parent.parent_id for parent in self.parents]
 
124
 
 
125
    def getLefthandParent(self):
 
126
        if len(self.parent_ids) == 0:
 
127
            parent_id = NULL_REVISION
 
128
        else:
 
129
            parent_id = self.parent_ids[0]
 
130
        return RevisionSet().getByRevisionId(parent_id)
 
131
 
 
132
    def getProperties(self):
 
133
        """See `IRevision`."""
 
134
        return dict((prop.name, prop.value) for prop in self.properties)
 
135
 
 
136
    def allocateKarma(self, branch):
 
137
        """See `IRevision`."""
 
138
        # If we know who the revision author is, give them karma.
 
139
        author = self.revision_author.person
 
140
        if author is not None:
 
141
            # Backdate the karma to the time the revision was created.  If the
 
142
            # revision_date on the revision is in future (for whatever weird
 
143
            # reason) we will use the date_created from the revision (which
 
144
            # will be now) as the karma date created.  Having future karma
 
145
            # events is both wrong, as the revision has been created (and it
 
146
            # is lying), and a problem with the way the Launchpad code
 
147
            # currently does its karma degradation over time.
 
148
            karma_date = min(self.revision_date, self.date_created)
 
149
            karma = branch.target.assignKarma(
 
150
                author, 'revisionadded', karma_date)
 
151
            if karma is not None:
 
152
                self.karma_allocated = True
 
153
            return karma
 
154
        else:
 
155
            return None
 
156
 
 
157
    def getBranch(self, allow_private=False, allow_junk=True):
 
158
        """See `IRevision`."""
 
159
        from lp.code.model.branch import Branch
 
160
        from lp.code.model.branchrevision import BranchRevision
 
161
 
 
162
        store = Store.of(self)
 
163
 
 
164
        query = And(
 
165
            self.id == BranchRevision.revision_id,
 
166
            BranchRevision.branch_id == Branch.id)
 
167
        if not allow_private:
 
168
            query = And(query, Not(Branch.transitively_private))
 
169
        if not allow_junk:
 
170
            query = And(
 
171
                query,
 
172
                # Not-junk branches are either associated with a product
 
173
                # or with a source package.
 
174
                Or(
 
175
                    (Branch.product != None),
 
176
                    And(
 
177
                        Branch.sourcepackagename != None,
 
178
                        Branch.distroseries != None)))
 
179
        result_set = store.find(Branch, query)
 
180
        if self.revision_author.person is None:
 
181
            result_set.order_by(Asc(BranchRevision.sequence))
 
182
        else:
 
183
            result_set.order_by(
 
184
                Branch.ownerID != self.revision_author.personID,
 
185
                Asc(BranchRevision.sequence))
 
186
 
 
187
        return result_set.first()
 
188
 
 
189
 
 
190
class RevisionAuthor(SQLBase):
 
191
    implements(IRevisionAuthor)
 
192
 
 
193
    _table = 'RevisionAuthor'
 
194
 
 
195
    name = StringCol(notNull=True, alternateID=True)
 
196
 
 
197
    @property
 
198
    def name_without_email(self):
 
199
        """Return the name of the revision author without the email address.
 
200
 
 
201
        If there is no name information (i.e. when the revision author only
 
202
        supplied their email address), return None.
 
203
        """
 
204
        if '@' not in self.name:
 
205
            return self.name
 
206
        return email.Utils.parseaddr(self.name)[0]
 
207
 
 
208
    email = StringCol(notNull=False, default=None)
 
209
    person = ForeignKey(dbName='person', foreignKey='Person', notNull=False,
 
210
                        storm_validator=validate_public_person, default=None)
 
211
 
 
212
    def linkToLaunchpadPerson(self):
 
213
        """See `IRevisionAuthor`."""
 
214
        if self.person is not None or self.email is None:
 
215
            return False
 
216
        lp_email = getUtility(IEmailAddressSet).getByEmail(self.email)
 
217
        # If not found, we didn't link this person.
 
218
        if lp_email is None:
 
219
            return False
 
220
        # Only accept an email address that is validated.
 
221
        if lp_email.status != EmailAddressStatus.NEW:
 
222
            self.personID = lp_email.personID
 
223
            return True
 
224
        else:
 
225
            return False
 
226
 
 
227
 
 
228
class RevisionParent(SQLBase):
 
229
    """The association between a revision and its parent."""
 
230
 
 
231
    implements(IRevisionParent)
 
232
 
 
233
    _table = 'RevisionParent'
 
234
 
 
235
    revision = ForeignKey(
 
236
        dbName='revision', foreignKey='Revision', notNull=True)
 
237
 
 
238
    sequence = IntCol(notNull=True)
 
239
    parent_id = StringCol(notNull=True)
 
240
 
 
241
 
 
242
class RevisionProperty(SQLBase):
 
243
    """A property on a revision. See IRevisionProperty."""
 
244
 
 
245
    implements(IRevisionProperty)
 
246
 
 
247
    _table = 'RevisionProperty'
 
248
 
 
249
    revision = ForeignKey(
 
250
        dbName='revision', foreignKey='Revision', notNull=True)
 
251
    name = StringCol(notNull=True)
 
252
    value = StringCol(notNull=True)
 
253
 
 
254
 
 
255
class RevisionSet:
 
256
 
 
257
    implements(IRevisionSet)
 
258
 
 
259
    def getByRevisionId(self, revision_id):
 
260
        return Revision.selectOneBy(revision_id=revision_id)
 
261
 
 
262
    def _createRevisionAuthor(self, revision_author):
 
263
        """Extract out the email and check to see if it matches a Person."""
 
264
        email_address = email.Utils.parseaddr(revision_author)[1]
 
265
        # If there is no @, then it isn't a real email address.
 
266
        if '@' not in email_address:
 
267
            email_address = None
 
268
 
 
269
        author = RevisionAuthor(name=revision_author, email=email_address)
 
270
        author.linkToLaunchpadPerson()
 
271
        return author
 
272
 
 
273
    def new(self, revision_id, log_body, revision_date, revision_author,
 
274
            parent_ids, properties, _date_created=None):
 
275
        """See IRevisionSet.new()"""
 
276
        if properties is None:
 
277
            properties = {}
 
278
        if _date_created is None:
 
279
            _date_created = UTC_NOW
 
280
        author = self.acquireRevisionAuthor(revision_author)
 
281
 
 
282
        revision = Revision(
 
283
            revision_id=revision_id,
 
284
            log_body=log_body,
 
285
            revision_date=revision_date,
 
286
            revision_author=author,
 
287
            date_created=_date_created)
 
288
        # Don't create future revisions.
 
289
        if revision.revision_date > revision.date_created:
 
290
            revision.revision_date = revision.date_created
 
291
 
 
292
        seen_parents = set()
 
293
        for sequence, parent_id in enumerate(parent_ids):
 
294
            if parent_id in seen_parents:
 
295
                continue
 
296
            seen_parents.add(parent_id)
 
297
            RevisionParent(revision=revision, sequence=sequence,
 
298
                           parent_id=parent_id)
 
299
 
 
300
        # Create revision properties.
 
301
        for name, value in properties.iteritems():
 
302
            RevisionProperty(revision=revision, name=name, value=value)
 
303
 
 
304
        return revision
 
305
 
 
306
    def acquireRevisionAuthor(self, name):
 
307
        """Find or create the RevisionAuthor with the specified name.
 
308
 
 
309
        Name may be any arbitrary string, but if it is an email-id, and
 
310
        its email address is a verified email address, it will be
 
311
        automatically linked to the corresponding Person.
 
312
 
 
313
        Email-ids come in two major forms:
 
314
            "Foo Bar" <foo@bar.com>
 
315
            foo@bar.com (Foo Bar)
 
316
        """
 
317
        # create a RevisionAuthor if necessary:
 
318
        try:
 
319
            return RevisionAuthor.byName(name)
 
320
        except SQLObjectNotFound:
 
321
            return self._createRevisionAuthor(name)
 
322
 
 
323
    def _timestampToDatetime(self, timestamp):
 
324
        """Convert the given timestamp to a datetime object.
 
325
 
 
326
        This works around a bug in Python that causes datetime.fromtimestamp
 
327
        to raise an exception if it is given a negative, fractional timestamp.
 
328
 
 
329
        :param timestamp: A timestamp from a bzrlib.revision.Revision
 
330
        :type timestamp: float
 
331
 
 
332
        :return: A datetime corresponding to the given timestamp.
 
333
        """
 
334
        # Work around Python bug #1646728.
 
335
        # See https://launchpad.net/bugs/81544.
 
336
        UTC = pytz.timezone('UTC')
 
337
        int_timestamp = int(timestamp)
 
338
        revision_date = datetime.fromtimestamp(int_timestamp, tz=UTC)
 
339
        revision_date += timedelta(seconds=timestamp - int_timestamp)
 
340
        return revision_date
 
341
 
 
342
    def newFromBazaarRevision(self, bzr_revision):
 
343
        """See `IRevisionSet`."""
 
344
        revision_id = bzr_revision.revision_id
 
345
        revision_date = self._timestampToDatetime(bzr_revision.timestamp)
 
346
        authors = bzr_revision.get_apparent_authors()
 
347
        # XXX: JonathanLange 2009-05-01 bug=362686: We can only have one
 
348
        # author per revision, so we use the first on the assumption that
 
349
        # this is the primary author.
 
350
        try:
 
351
            author = authors[0]
 
352
        except IndexError:
 
353
            author = None
 
354
        return self.new(
 
355
            revision_id=revision_id,
 
356
            log_body=bzr_revision.message,
 
357
            revision_date=revision_date,
 
358
            revision_author=author,
 
359
            parent_ids=bzr_revision.parent_ids,
 
360
            properties=bzr_revision.properties)
 
361
 
 
362
    @staticmethod
 
363
    def onlyPresent(revids):
 
364
        """See `IRevisionSet`."""
 
365
        if not revids:
 
366
            return set()
 
367
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
 
368
        store.execute(
 
369
            """
 
370
            CREATE TEMPORARY TABLE Revids
 
371
            (revision_id text)
 
372
            """)
 
373
        data = []
 
374
        for revid in revids:
 
375
            data.append('(%s)' % sqlvalues(revid))
 
376
        data = ', '.join(data)
 
377
        store.execute(
 
378
            "INSERT INTO Revids (revision_id) VALUES %s" % data)
 
379
        result = store.execute(
 
380
            """
 
381
            SELECT Revids.revision_id
 
382
            FROM Revids, Revision
 
383
            WHERE Revids.revision_id = Revision.revision_id
 
384
            """)
 
385
        present = set()
 
386
        for row in result.get_all():
 
387
            present.add(row[0])
 
388
        store.execute("DROP TABLE Revids")
 
389
        return present
 
390
 
 
391
    def checkNewVerifiedEmail(self, email):
 
392
        """See `IRevisionSet`."""
 
393
        # Bypass zope's security because IEmailAddress.email is not public.
 
394
        naked_email = removeSecurityProxy(email)
 
395
        for author in RevisionAuthor.selectBy(email=naked_email.email):
 
396
            author.personID = email.personID
 
397
 
 
398
    def getTipRevisionsForBranches(self, branches):
 
399
        """See `IRevisionSet`."""
 
400
        # If there are no branch_ids, then return None.
 
401
        branch_ids = [branch.id for branch in branches]
 
402
        if not branch_ids:
 
403
            return None
 
404
        return Revision.select("""
 
405
            Branch.id in %s AND
 
406
            Revision.revision_id = Branch.last_scanned_id
 
407
            """ % quote(branch_ids),
 
408
            clauseTables=['Branch'], prejoins=['revision_author'])
 
409
 
 
410
    @staticmethod
 
411
    def getRecentRevisionsForProduct(product, days):
 
412
        """See `IRevisionSet`."""
 
413
        # Here to stop circular imports.
 
414
        from lp.code.model.branch import Branch
 
415
        from lp.code.model.branchrevision import BranchRevision
 
416
 
 
417
        revision_subselect = Select(
 
418
            Min(Revision.id), revision_time_limit(days))
 
419
        # Only look in active branches.
 
420
        result_set = Store.of(product).find(
 
421
            (Revision, RevisionAuthor),
 
422
            Revision.revision_author == RevisionAuthor.id,
 
423
            revision_time_limit(days),
 
424
            BranchRevision.revision == Revision.id,
 
425
            BranchRevision.branch == Branch.id,
 
426
            Branch.product == product,
 
427
            Branch.lifecycle_status.is_in(DEFAULT_BRANCH_STATUS_IN_LISTING),
 
428
            BranchRevision.revision_id >= revision_subselect)
 
429
        result_set.config(distinct=True)
 
430
        return result_set.order_by(Desc(Revision.revision_date))
 
431
 
 
432
    @staticmethod
 
433
    def getRevisionsNeedingKarmaAllocated(limit=None):
 
434
        """See `IRevisionSet`."""
 
435
        # Here to stop circular imports.
 
436
        from lp.code.model.branch import Branch
 
437
        from lp.code.model.branchrevision import BranchRevision
 
438
 
 
439
        store = IStore(Revision)
 
440
        results_with_dupes = store.find(
 
441
            Revision,
 
442
            Revision.revision_author == RevisionAuthor.id,
 
443
            RevisionAuthor.person == ValidPersonCache.id,
 
444
            Not(Revision.karma_allocated),
 
445
            BranchRevision.revision == Revision.id,
 
446
            BranchRevision.branch == Branch.id,
 
447
            Or(Branch.product != None, Branch.distroseries != None))[:limit]
 
448
        # Eliminate duplicate rows, returning <= limit rows
 
449
        return store.find(
 
450
            Revision, Revision.id.is_in(
 
451
                results_with_dupes.get_select_expr(Revision.id)))
 
452
 
 
453
    @staticmethod
 
454
    def getPublicRevisionsForPerson(person, day_limit=30):
 
455
        """See `IRevisionSet`."""
 
456
        # Here to stop circular imports.
 
457
        from lp.code.model.branch import Branch
 
458
        from lp.code.model.branchrevision import BranchRevision
 
459
        from lp.registry.model.teammembership import (
 
460
            TeamParticipation)
 
461
 
 
462
        store = Store.of(person)
 
463
 
 
464
        origin = [
 
465
            Revision,
 
466
            Join(BranchRevision, BranchRevision.revision == Revision.id),
 
467
            Join(Branch, BranchRevision.branch == Branch.id),
 
468
            Join(RevisionAuthor,
 
469
                 Revision.revision_author == RevisionAuthor.id),
 
470
            ]
 
471
 
 
472
        if person.is_team:
 
473
            origin.append(
 
474
                Join(TeamParticipation,
 
475
                     RevisionAuthor.personID == TeamParticipation.personID))
 
476
            person_condition = TeamParticipation.team == person
 
477
        else:
 
478
            person_condition = RevisionAuthor.person == person
 
479
 
 
480
        result_set = store.using(*origin).find(
 
481
            Revision,
 
482
            And(revision_time_limit(day_limit),
 
483
                person_condition,
 
484
                Not(Branch.transitively_private)))
 
485
        result_set.config(distinct=True)
 
486
        return result_set.order_by(Desc(Revision.revision_date))
 
487
 
 
488
    @staticmethod
 
489
    def _getPublicRevisionsHelper(obj, day_limit):
 
490
        """Helper method for Products and ProjectGroups."""
 
491
        # Here to stop circular imports.
 
492
        from lp.code.model.branch import Branch
 
493
        from lp.registry.model.product import Product
 
494
        from lp.code.model.branchrevision import BranchRevision
 
495
 
 
496
        origin = [
 
497
            Revision,
 
498
            Join(BranchRevision, BranchRevision.revision == Revision.id),
 
499
            Join(Branch, BranchRevision.branch == Branch.id),
 
500
            ]
 
501
 
 
502
        conditions = And(revision_time_limit(day_limit),
 
503
                         Not(Branch.transitively_private))
 
504
 
 
505
        if IProduct.providedBy(obj):
 
506
            conditions = And(conditions, Branch.product == obj)
 
507
        elif IProjectGroup.providedBy(obj):
 
508
            origin.append(Join(Product, Branch.product == Product.id))
 
509
            conditions = And(conditions, Product.project == obj)
 
510
        else:
 
511
            raise AssertionError(
 
512
                "Not an IProduct or IProjectGroup: %r" % obj)
 
513
 
 
514
        result_set = Store.of(obj).using(*origin).find(
 
515
            Revision, conditions)
 
516
        result_set.config(distinct=True)
 
517
        return result_set.order_by(Desc(Revision.revision_date))
 
518
 
 
519
    @classmethod
 
520
    def getPublicRevisionsForProduct(cls, product, day_limit=30):
 
521
        """See `IRevisionSet`."""
 
522
        return cls._getPublicRevisionsHelper(product, day_limit)
 
523
 
 
524
    @classmethod
 
525
    def getPublicRevisionsForProjectGroup(cls, project, day_limit=30):
 
526
        """See `IRevisionSet`."""
 
527
        return cls._getPublicRevisionsHelper(project, day_limit)
 
528
 
 
529
    @staticmethod
 
530
    def updateRevisionCacheForBranch(branch):
 
531
        """See `IRevisionSet`."""
 
532
        # Hand crafting the sql insert statement as storm doesn't handle the
 
533
        # INSERT INTO ... SELECT ... syntax.  Also there is no public api yet
 
534
        # for storm to get the select statement.
 
535
 
 
536
        # Remove the security proxy to get access to the ID columns.
 
537
        naked_branch = removeSecurityProxy(branch)
 
538
 
 
539
        insert_columns = ['Revision.id', 'revision_author', 'revision_date']
 
540
        subselect_clauses = []
 
541
        if branch.product is None:
 
542
            insert_columns.append('NULL')
 
543
            subselect_clauses.append('product IS NULL')
 
544
        else:
 
545
            insert_columns.append(str(naked_branch.productID))
 
546
            subselect_clauses.append('product = %s' % naked_branch.productID)
 
547
 
 
548
        if branch.distroseries is None:
 
549
            insert_columns.extend(['NULL', 'NULL'])
 
550
            subselect_clauses.extend(
 
551
                ['distroseries IS NULL', 'sourcepackagename IS NULL'])
 
552
        else:
 
553
            insert_columns.extend(
 
554
                [str(naked_branch.distroseriesID),
 
555
                 str(naked_branch.sourcepackagenameID)])
 
556
            subselect_clauses.extend(
 
557
                ['distroseries = %s' % naked_branch.distroseriesID,
 
558
                 'sourcepackagename = %s' % naked_branch.sourcepackagenameID])
 
559
 
 
560
        insert_columns.append(str(branch.private))
 
561
        if branch.private:
 
562
            subselect_clauses.append('private IS TRUE')
 
563
        else:
 
564
            subselect_clauses.append('private IS FALSE')
 
565
 
 
566
        insert_statement = """
 
567
            INSERT INTO RevisionCache
 
568
            (revision, revision_author, revision_date,
 
569
             product, distroseries, sourcepackagename, private)
 
570
            SELECT %(columns)s FROM Revision
 
571
            JOIN BranchRevision ON BranchRevision.revision = Revision.id
 
572
            WHERE Revision.revision_date > (
 
573
                CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - interval '30 days')
 
574
            AND BranchRevision.branch = %(branch_id)s
 
575
            AND Revision.id NOT IN (
 
576
                SELECT revision FROM RevisionCache
 
577
                WHERE %(subselect_where)s)
 
578
            """ % {
 
579
            'columns': ', '.join(insert_columns),
 
580
            'branch_id': branch.id,
 
581
            'subselect_where': ' AND '.join(subselect_clauses),
 
582
            }
 
583
        Store.of(branch).execute(insert_statement)
 
584
 
 
585
    @staticmethod
 
586
    def pruneRevisionCache(limit):
 
587
        """See `IRevisionSet`."""
 
588
        # Storm doesn't handle remove a limited result set:
 
589
        #    FeatureError: Can't remove a sliced result set
 
590
        store = IMasterStore(RevisionCache)
 
591
        epoch = datetime.now(tz=pytz.UTC) - timedelta(days=30)
 
592
        subquery = Select(
 
593
            [RevisionCache.id],
 
594
            RevisionCache.revision_date < epoch,
 
595
            limit=limit)
 
596
        store.find(RevisionCache, RevisionCache.id.is_in(subquery)).remove()
 
597
 
 
598
 
 
599
def revision_time_limit(day_limit):
 
600
    """The storm fragment to limit the revision_date field of the Revision."""
 
601
    now = datetime.now(pytz.UTC)
 
602
    earliest = now - timedelta(days=day_limit)
 
603
 
 
604
    return And(
 
605
        Revision.revision_date <= now,
 
606
        Revision.revision_date > earliest)
 
607
 
 
608
 
 
609
class RevisionCache(Storm):
 
610
    """A cached version of a recent revision."""
 
611
 
 
612
    __storm_table__ = 'RevisionCache'
 
613
 
 
614
    id = Int(primary=True)
 
615
 
 
616
    revision_id = Int(name='revision', allow_none=False)
 
617
    revision = Reference(revision_id, 'Revision.id')
 
618
 
 
619
    revision_author_id = Int(name='revision_author', allow_none=False)
 
620
    revision_author = Reference(revision_author_id, 'RevisionAuthor.id')
 
621
 
 
622
    revision_date = UtcDateTimeCol(notNull=True)
 
623
 
 
624
    product_id = Int(name='product', allow_none=True)
 
625
    product = Reference(product_id, 'Product.id')
 
626
 
 
627
    distroseries_id = Int(name='distroseries', allow_none=True)
 
628
    distroseries = Reference(distroseries_id, 'DistroSeries.id')
 
629
 
 
630
    sourcepackagename_id = Int(name='sourcepackagename', allow_none=True)
 
631
    sourcepackagename = Reference(
 
632
        sourcepackagename_id, 'SourcePackageName.id')
 
633
 
 
634
    private = Bool(allow_none=False, default=False)
 
635
 
 
636
    def __init__(self, revision):
 
637
        # Make the revision_author assignment first as traversing to the
 
638
        # revision_author of the revision does a query which causes a store
 
639
        # flush.  If an assignment has been done already, the RevisionCache
 
640
        # object would have been implicitly added to the store, and failes
 
641
        # with an integrity check.
 
642
        self.revision_author = revision.revision_author
 
643
        self.revision = revision
 
644
        self.revision_date = revision.revision_date