~launchpad-pqm/launchpad/devel

14538.1.3 by Curtis Hovey
Updated copyright.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.17 by Karl Fogel
Add the copyright header block to the rest of the files under lib/lp/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4983.1.2 by Curtis Hovey
Added pylint exceptions to database classes.
4
# pylint: disable-msg=E0611,W0212
1102.1.63 by David Allouche
Revision and RevisionAuthor, remove ArchUserID
5
6
__metaclass__ = type
3849.1.22 by jml at canonical
Add RevisionProperty class and interface.
7
__all__ = [
7675.169.3 by Tim Penhey
Add methods to populate and prune the RevisionCache.
8
    'Revision',
9
    'RevisionAuthor',
10
    'RevisionCache',
11
    'RevisionParent',
12
    'RevisionProperty',
3849.1.22 by jml at canonical
Add RevisionProperty class and interface.
13
    'RevisionSet']
1102.1.63 by David Allouche
Revision and RevisionAuthor, remove ArchUserID
14
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
15
from datetime import (
16
    datetime,
17
    timedelta,
18
    )
4177.3.2 by Jonathan Lange
Add 'getNameWithoutEmail' to get just the name of the revision author.
19
import email
20
11486.2.4 by Aaron Bentley
Add Revision.getLefthandParent.
21
from bzrlib.revision import NULL_REVISION
6096.5.5 by Tim Penhey
initial work
22
import pytz
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
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
    )
6736.3.1 by Tim Penhey
Initial work.
47
from storm.store import Store
5543.7.3 by Tim Penhey
New revisions now check to see if we know the author.
48
from zope.component import getUtility
1102.1.63 by David Allouche
Revision and RevisionAuthor, remove ArchUserID
49
from zope.interface import implements
7675.169.3 by Tim Penhey
Add methods to populate and prune the RevisionCache.
50
from zope.security.proxy import removeSecurityProxy
1102.1.63 by David Allouche
Revision and RevisionAuthor, remove ArchUserID
51
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
52
from canonical.database.constants import (
53
    DEFAULT,
54
    UTC_NOW,
55
    )
7925.4.1 by Tim Penhey
Don't create revisions with future revision_dates.
56
from canonical.database.datetimecol import UtcDateTimeCol
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
57
from canonical.database.sqlbase import (
58
    quote,
59
    SQLBase,
60
    sqlvalues,
61
    )
8590.2.2 by Tim Penhey
Fix the fallout in the zcml and other imports.
62
from canonical.launchpad.helpers import shortlist
14538.1.2 by Curtis Hovey
Moved account and email address to lp.services.identity.
63
from lp.services.identity.interfaces.emailaddress import (
11262.3.4 by Curtis Hovey
On import per line.
64
    EmailAddressStatus,
65
    IEmailAddressSet,
66
    )
11693.1.1 by Stuart Bishop
Fix performance of revision karma allocator under PostgreSQL 8.4
67
from canonical.launchpad.interfaces.lpstorm import IMasterStore, IStore
8590.2.2 by Tim Penhey
Fix the fallout in the zcml and other imports.
68
from canonical.launchpad.webapp.interfaces import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
69
    DEFAULT_FLAVOR,
70
    IStoreSelector,
71
    MAIN_STORE,
72
    )
8590.2.2 by Tim Penhey
Fix the fallout in the zcml and other imports.
73
from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
74
from lp.code.interfaces.revision import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
75
    IRevision,
76
    IRevisionAuthor,
77
    IRevisionParent,
78
    IRevisionProperty,
79
    IRevisionSet,
80
    )
81
from lp.registry.interfaces.person import validate_public_person
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
82
from lp.registry.interfaces.product import IProduct
10326.1.2 by Henning Eggers
Renamed project interfaces module to projectgroup.
83
from lp.registry.interfaces.projectgroup import IProjectGroup
12337.2.10 by Jeroen Vermeulen
Extend and reuse PersonSet.getPrecachedPersonsFromIDs.
84
from lp.registry.model.person import ValidPersonCache
1102.1.63 by David Allouche
Revision and RevisionAuthor, remove ArchUserID
85
86
87
class Revision(SQLBase):
88
    """See IRevision."""
89
90
    implements(IRevision)
91
92
    date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)
93
    log_body = StringCol(notNull=True)
1102.1.143 by David Allouche
Review fixes to Branch view.
94
    gpgkey = ForeignKey(dbName='gpgkey', foreignKey='GPGKey', default=None)
95
12337.2.3 by Jeroen Vermeulen
Use formatter for RevisionAuthor; prefetch author info.
96
    revision_author_id = Int(name='revision_author', allow_none=False)
97
    revision_author = Reference(revision_author_id, 'RevisionAuthor.id')
98
3504.2.10 by James Henstridge
changes suggested by spiv in review
99
    revision_id = StringCol(notNull=True, alternateID=True,
100
                            alternateMethodName='byRevisionID')
1102.1.63 by David Allouche
Revision and RevisionAuthor, remove ArchUserID
101
    revision_date = UtcDateTimeCol(notNull=False)
102
6623.4.1 by Tim Penhey
Initial karma allocation done. TODO: karma when revision author linked.
103
    karma_allocated = BoolCol(default=False, notNull=True)
104
3849.1.22 by jml at canonical
Add RevisionProperty class and interface.
105
    properties = SQLMultipleJoin('RevisionProperty', joinColumn='revision')
106
1102.1.123 by david
schema supports ghost revisions
107
    @property
3691.25.5 by James Henstridge
get rid of direct RevisionParent use in bzrsync
108
    def parents(self):
109
        """See IRevision.parents"""
110
        return shortlist(RevisionParent.selectBy(
3691.62.21 by kiko
Clean up the use of ID/.id in select*By and constructors
111
            revision=self, orderBy='sequence'))
3691.25.5 by James Henstridge
get rid of direct RevisionParent use in bzrsync
112
113
    @property
1102.1.123 by david
schema supports ghost revisions
114
    def parent_ids(self):
1102.1.147 by David Allouche
review fixes: launchpad.database.revision
115
        """Sequence of globally unique ids for the parents of this revision.
116
117
        The corresponding Revision objects can be retrieved, if they are
118
        present in the database, using the RevisionSet Zope utility.
119
        """
3691.25.5 by James Henstridge
get rid of direct RevisionParent use in bzrsync
120
        return [parent.parent_id for parent in self.parents]
1102.1.116 by david
TODOs for branch.txt and revision.py
121
11486.2.4 by Aaron Bentley
Add Revision.getLefthandParent.
122
    def getLefthandParent(self):
123
        if len(self.parent_ids) == 0:
124
            parent_id = NULL_REVISION
125
        else:
126
            parent_id = self.parent_ids[0]
127
        return RevisionSet().getByRevisionId(parent_id)
128
3849.1.23 by jml at canonical
Add getProperties method to return the properties as a dict.
129
    def getProperties(self):
6623.4.1 by Tim Penhey
Initial karma allocation done. TODO: karma when revision author linked.
130
        """See `IRevision`."""
3849.1.23 by jml at canonical
Add getProperties method to return the properties as a dict.
131
        return dict((prop.name, prop.value) for prop in self.properties)
132
6623.4.1 by Tim Penhey
Initial karma allocation done. TODO: karma when revision author linked.
133
    def allocateKarma(self, branch):
134
        """See `IRevision`."""
135
        # If we know who the revision author is, give them karma.
136
        author = self.revision_author.person
7675.439.12 by Tim Penhey
Remove unneeded parentheses.
137
        if author is not None:
6950.1.3 by Tim Penhey
Updates following review.
138
            # Backdate the karma to the time the revision was created.  If the
139
            # revision_date on the revision is in future (for whatever weird
140
            # reason) we will use the date_created from the revision (which
141
            # will be now) as the karma date created.  Having future karma
142
            # events is both wrong, as the revision has been created (and it
143
            # is lying), and a problem with the way the Launchpad code
144
            # currently does its karma degradation over time.
7675.439.13 by Tim Penhey
change how we allocate backdated karma
145
            karma_date = min(self.revision_date, self.date_created)
146
            karma = branch.target.assignKarma(
147
                author, 'revisionadded', karma_date)
6623.4.23 by Tim Penhey
Fix a bug with attempting to allocate karma to an inactive person, and fix logging in the cron script.
148
            if karma is not None:
149
                self.karma_allocated = True
7675.439.5 by Tim Penhey
Make the test_karmaDateForFutureRevisions actually test what it says it is testing.
150
            return karma
151
        else:
152
            return None
6623.4.1 by Tim Penhey
Initial karma allocation done. TODO: karma when revision author linked.
153
6623.4.25 by Tim Penhey
Make sure we don't try to allocate karma to junk branches.
154
    def getBranch(self, allow_private=False, allow_junk=True):
6736.3.4 by Tim Penhey
Added tests for addition Revision and RevisionSet methods.
155
        """See `IRevision`."""
8138.1.2 by Jonathan Lange
Run migrater over lp.code. Many tests broken and imports failing.
156
        from lp.code.model.branch import Branch
157
        from lp.code.model.branchrevision import BranchRevision
6736.3.4 by Tim Penhey
Added tests for addition Revision and RevisionSet methods.
158
159
        store = Store.of(self)
160
6623.4.6 by Tim Penhey
Hook up the claiming of older revisions.
161
        query = And(
7675.747.1 by Jeroen Vermeulen
Intermediate state: stormifying BranchRevision.
162
            self.id == BranchRevision.revision_id,
163
            BranchRevision.branch_id == Branch.id)
13931.4.3 by Ian Booth
Add new transitively_private attribute on branch
164
        if not allow_private:
165
            query = And(query, Not(Branch.transitively_private))
13875.2.2 by Ian Booth
Use new Storm recursive With and make implementation more efficient by using early filtering
166
        if not allow_junk:
13931.4.3 by Ian Booth
Add new transitively_private attribute on branch
167
            query = And(
168
                query,
169
                # Not-junk branches are either associated with a product
170
                # or with a source package.
171
                Or(
13875.2.2 by Ian Booth
Use new Storm recursive With and make implementation more efficient by using early filtering
172
                    (Branch.product != None),
173
                    And(
174
                        Branch.sourcepackagename != None,
13931.4.3 by Ian Booth
Add new transitively_private attribute on branch
175
                        Branch.distroseries != None)))
6623.4.6 by Tim Penhey
Hook up the claiming of older revisions.
176
        result_set = store.find(Branch, query)
6736.3.4 by Tim Penhey
Added tests for addition Revision and RevisionSet methods.
177
        if self.revision_author.person is None:
178
            result_set.order_by(Asc(BranchRevision.sequence))
179
        else:
180
            result_set.order_by(
181
                Branch.ownerID != self.revision_author.personID,
182
                Asc(BranchRevision.sequence))
183
184
        return result_set.first()
185
1102.1.63 by David Allouche
Revision and RevisionAuthor, remove ArchUserID
186
187
class RevisionAuthor(SQLBase):
188
    implements(IRevisionAuthor)
189
190
    _table = 'RevisionAuthor'
191
3504.2.10 by James Henstridge
changes suggested by spiv in review
192
    name = StringCol(notNull=True, alternateID=True)
1102.1.63 by David Allouche
Revision and RevisionAuthor, remove ArchUserID
193
5743.2.6 by Tim Penhey
Updates following review
194
    @property
195
    def name_without_email(self):
4177.3.2 by Jonathan Lange
Add 'getNameWithoutEmail' to get just the name of the revision author.
196
        """Return the name of the revision author without the email address.
197
198
        If there is no name information (i.e. when the revision author only
199
        supplied their email address), return None.
200
        """
5743.2.2 by Tim Penhey
Listing now has revision number and codebrowse hyperlink.
201
        if '@' not in self.name:
202
            return self.name
4177.3.9 by Jonathan Lange
Return empty string from name_without_email if there's no name, and display
203
        return email.Utils.parseaddr(self.name)[0]
4177.3.2 by Jonathan Lange
Add 'getNameWithoutEmail' to get just the name of the revision author.
204
5543.7.8 by Tim Penhey
Giving email a default.
205
    email = StringCol(notNull=False, default=None)
5543.7.3 by Tim Penhey
New revisions now check to see if we know the author.
206
    person = ForeignKey(dbName='person', foreignKey='Person', notNull=False,
5821.2.40 by James Henstridge
* Move all the uses of public_person_validator over to the Storm
207
                        storm_validator=validate_public_person, default=None)
5543.7.3 by Tim Penhey
New revisions now check to see if we know the author.
208
209
    def linkToLaunchpadPerson(self):
210
        """See `IRevisionAuthor`."""
211
        if self.person is not None or self.email is None:
212
            return False
213
        lp_email = getUtility(IEmailAddressSet).getByEmail(self.email)
214
        # If not found, we didn't link this person.
215
        if lp_email is None:
216
            return False
217
        # Only accept an email address that is validated.
5543.7.4 by Tim Penhey
All done.
218
        if lp_email.status != EmailAddressStatus.NEW:
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
219
            self.personID = lp_email.personID
5543.7.3 by Tim Penhey
New revisions now check to see if we know the author.
220
            return True
221
        else:
222
            return False
5543.7.2 by Tim Penhey
Removed the Revision.author field, and add interface and database bits for RevisionAuthor email and person fields.
223
1102.1.123 by david
schema supports ghost revisions
224
225
class RevisionParent(SQLBase):
226
    """The association between a revision and its parent."""
227
228
    implements(IRevisionParent)
229
230
    _table = 'RevisionParent'
231
232
    revision = ForeignKey(
233
        dbName='revision', foreignKey='Revision', notNull=True)
1102.1.147 by David Allouche
review fixes: launchpad.database.revision
234
1102.1.123 by david
schema supports ghost revisions
235
    sequence = IntCol(notNull=True)
236
    parent_id = StringCol(notNull=True)
237
238
3849.1.22 by jml at canonical
Add RevisionProperty class and interface.
239
class RevisionProperty(SQLBase):
240
    """A property on a revision. See IRevisionProperty."""
241
242
    implements(IRevisionProperty)
243
244
    _table = 'RevisionProperty'
245
246
    revision = ForeignKey(
247
        dbName='revision', foreignKey='Revision', notNull=True)
248
    name = StringCol(notNull=True)
249
    value = StringCol(notNull=True)
250
251
1102.1.126 by david
add RevisionSet
252
class RevisionSet:
253
254
    implements(IRevisionSet)
255
256
    def getByRevisionId(self, revision_id):
257
        return Revision.selectOneBy(revision_id=revision_id)
3691.25.3 by James Henstridge
add IRevisionSet.new()
258
5543.7.3 by Tim Penhey
New revisions now check to see if we know the author.
259
    def _createRevisionAuthor(self, revision_author):
260
        """Extract out the email and check to see if it matches a Person."""
261
        email_address = email.Utils.parseaddr(revision_author)[1]
262
        # If there is no @, then it isn't a real email address.
263
        if '@' not in email_address:
264
            email_address = None
5543.7.4 by Tim Penhey
All done.
265
266
        author = RevisionAuthor(name=revision_author, email=email_address)
267
        author.linkToLaunchpadPerson()
268
        return author
5543.7.3 by Tim Penhey
New revisions now check to see if we know the author.
269
5485.1.18 by Edwin Grubbs
Fixed lots of lint issues
270
    def new(self, revision_id, log_body, revision_date, revision_author,
9984.4.2 by Tim Penhey
Allow the passing of date_created to the factory method to create revisions.
271
            parent_ids, properties, _date_created=None):
3691.25.3 by James Henstridge
add IRevisionSet.new()
272
        """See IRevisionSet.new()"""
3849.1.25 by jml at canonical
Store the revision properties.
273
        if properties is None:
274
            properties = {}
9984.4.2 by Tim Penhey
Allow the passing of date_created to the factory method to create revisions.
275
        if _date_created is None:
276
            _date_created = UTC_NOW
8716.4.2 by Aaron Bentley
Use Revision authors and Peson.unique_displayname where possible.
277
        author = self.acquireRevisionAuthor(revision_author)
3691.25.3 by James Henstridge
add IRevisionSet.new()
278
9984.4.2 by Tim Penhey
Allow the passing of date_created to the factory method to create revisions.
279
        revision = Revision(
280
            revision_id=revision_id,
281
            log_body=log_body,
282
            revision_date=revision_date,
283
            revision_author=author,
284
            date_created=_date_created)
7925.4.1 by Tim Penhey
Don't create revisions with future revision_dates.
285
        # Don't create future revisions.
286
        if revision.revision_date > revision.date_created:
287
            revision.revision_date = revision.date_created
288
3691.25.3 by James Henstridge
add IRevisionSet.new()
289
        seen_parents = set()
290
        for sequence, parent_id in enumerate(parent_ids):
291
            if parent_id in seen_parents:
292
                continue
293
            seen_parents.add(parent_id)
3691.62.21 by kiko
Clean up the use of ID/.id in select*By and constructors
294
            RevisionParent(revision=revision, sequence=sequence,
3691.25.3 by James Henstridge
add IRevisionSet.new()
295
                           parent_id=parent_id)
3849.1.25 by jml at canonical
Store the revision properties.
296
297
        # Create revision properties.
298
        for name, value in properties.iteritems():
299
            RevisionProperty(revision=revision, name=name, value=value)
300
3691.25.3 by James Henstridge
add IRevisionSet.new()
301
        return revision
5543.7.4 by Tim Penhey
All done.
302
8716.4.2 by Aaron Bentley
Use Revision authors and Peson.unique_displayname where possible.
303
    def acquireRevisionAuthor(self, name):
304
        """Find or create the RevisionAuthor with the specified name.
305
306
        Name may be any arbitrary string, but if it is an email-id, and
307
        its email address is a verified email address, it will be
308
        automatically linked to the corresponding Person.
309
310
        Email-ids come in two major forms:
311
            "Foo Bar" <foo@bar.com>
312
            foo@bar.com (Foo Bar)
313
        """
314
        # create a RevisionAuthor if necessary:
315
        try:
316
            return RevisionAuthor.byName(name)
317
        except SQLObjectNotFound:
318
            return self._createRevisionAuthor(name)
319
6789.3.9 by Jonathan Lange
Move the revision from bzr revision method into RevisionSet.
320
    def _timestampToDatetime(self, timestamp):
321
        """Convert the given timestamp to a datetime object.
322
323
        This works around a bug in Python that causes datetime.fromtimestamp
324
        to raise an exception if it is given a negative, fractional timestamp.
325
326
        :param timestamp: A timestamp from a bzrlib.revision.Revision
327
        :type timestamp: float
328
329
        :return: A datetime corresponding to the given timestamp.
330
        """
331
        # Work around Python bug #1646728.
332
        # See https://launchpad.net/bugs/81544.
333
        UTC = pytz.timezone('UTC')
334
        int_timestamp = int(timestamp)
335
        revision_date = datetime.fromtimestamp(int_timestamp, tz=UTC)
336
        revision_date += timedelta(seconds=timestamp - int_timestamp)
337
        return revision_date
338
339
    def newFromBazaarRevision(self, bzr_revision):
340
        """See `IRevisionSet`."""
341
        revision_id = bzr_revision.revision_id
342
        revision_date = self._timestampToDatetime(bzr_revision.timestamp)
8303.5.3 by Jonathan Lange
Looks like revisions might not actually have authors.
343
        authors = bzr_revision.get_apparent_authors()
344
        # XXX: JonathanLange 2009-05-01 bug=362686: We can only have one
345
        # author per revision, so we use the first on the assumption that
346
        # this is the primary author.
347
        try:
13269.2.6 by Jonathan Lange
Avoid unnecessary work.
348
            author = authors[0]
8303.5.3 by Jonathan Lange
Looks like revisions might not actually have authors.
349
        except IndexError:
350
            author = None
6789.3.9 by Jonathan Lange
Move the revision from bzr revision method into RevisionSet.
351
        return self.new(
352
            revision_id=revision_id,
353
            log_body=bzr_revision.message,
354
            revision_date=revision_date,
8303.5.3 by Jonathan Lange
Looks like revisions might not actually have authors.
355
            revision_author=author,
6789.3.9 by Jonathan Lange
Move the revision from bzr revision method into RevisionSet.
356
            parent_ids=bzr_revision.parent_ids,
357
            properties=bzr_revision.properties)
358
7049.1.1 by Michael Hudson
hacking graph yoga to determine revisions to insert
359
    @staticmethod
7049.1.9 by Michael Hudson
more tidying
360
    def onlyPresent(revids):
7049.1.18 by Michael Hudson
review comments
361
        """See `IRevisionSet`."""
7049.1.10 by Michael Hudson
more sql, less graph yoga
362
        if not revids:
363
            return set()
7049.1.1 by Michael Hudson
hacking graph yoga to determine revisions to insert
364
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
7049.1.10 by Michael Hudson
more sql, less graph yoga
365
        store.execute(
366
            """
367
            CREATE TEMPORARY TABLE Revids
368
            (revision_id text)
369
            """)
370
        data = []
7049.1.16 by Michael Hudson
docstrings, comments
371
        for revid in revids:
372
            data.append('(%s)' % sqlvalues(revid))
7049.1.10 by Michael Hudson
more sql, less graph yoga
373
        data = ', '.join(data)
374
        store.execute(
375
            "INSERT INTO Revids (revision_id) VALUES %s" % data)
376
        result = store.execute(
377
            """
378
            SELECT Revids.revision_id
379
            FROM Revids, Revision
380
            WHERE Revids.revision_id = Revision.revision_id
381
            """)
382
        present = set()
383
        for row in result.get_all():
384
            present.add(row[0])
385
        store.execute("DROP TABLE Revids")
386
        return present
7049.1.1 by Michael Hudson
hacking graph yoga to determine revisions to insert
387
5543.7.4 by Tim Penhey
All done.
388
    def checkNewVerifiedEmail(self, email):
389
        """See `IRevisionSet`."""
6596.1.3 by Guilherme Salgado
Some small changes requested by Gavin.
390
        # Bypass zope's security because IEmailAddress.email is not public.
6596.1.1 by Guilherme Salgado
Fix https://launchpad.net/bugs/241332
391
        naked_email = removeSecurityProxy(email)
392
        for author in RevisionAuthor.selectBy(email=naked_email.email):
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
393
            author.personID = email.personID
5743.2.2 by Tim Penhey
Listing now has revision number and codebrowse hyperlink.
394
395
    def getTipRevisionsForBranches(self, branches):
396
        """See `IRevisionSet`."""
5743.2.6 by Tim Penhey
Updates following review
397
        # If there are no branch_ids, then return None.
5743.2.2 by Tim Penhey
Listing now has revision number and codebrowse hyperlink.
398
        branch_ids = [branch.id for branch in branches]
399
        if not branch_ids:
5743.2.6 by Tim Penhey
Updates following review
400
            return None
5743.2.2 by Tim Penhey
Listing now has revision number and codebrowse hyperlink.
401
        return Revision.select("""
402
            Branch.id in %s AND
403
            Revision.revision_id = Branch.last_scanned_id
404
            """ % quote(branch_ids),
405
            clauseTables=['Branch'], prejoins=['revision_author'])
6096.5.5 by Tim Penhey
initial work
406
7028.1.1 by Tim Penhey
Speed up the query that is used to count the number of new revisions shown on the +code-index and +branches pages for products.
407
    @staticmethod
408
    def getRecentRevisionsForProduct(product, days):
6096.5.5 by Tim Penhey
initial work
409
        """See `IRevisionSet`."""
7028.1.1 by Tim Penhey
Speed up the query that is used to count the number of new revisions shown on the +code-index and +branches pages for products.
410
        # Here to stop circular imports.
8138.1.2 by Jonathan Lange
Run migrater over lp.code. Many tests broken and imports failing.
411
        from lp.code.model.branch import Branch
412
        from lp.code.model.branchrevision import BranchRevision
7028.1.1 by Tim Penhey
Speed up the query that is used to count the number of new revisions shown on the +code-index and +branches pages for products.
413
7381.1.2 by Paul Hummer
Refactored Revision.getRecentRevisionsForProduct
414
        revision_subselect = Select(
415
            Min(Revision.id), revision_time_limit(days))
8079.2.2 by Tim Penhey
Only look in active branches for recent revisions.
416
        # Only look in active branches.
7381.1.2 by Paul Hummer
Refactored Revision.getRecentRevisionsForProduct
417
        result_set = Store.of(product).find(
7028.1.1 by Tim Penhey
Speed up the query that is used to count the number of new revisions shown on the +code-index and +branches pages for products.
418
            (Revision, RevisionAuthor),
7381.1.2 by Paul Hummer
Refactored Revision.getRecentRevisionsForProduct
419
            Revision.revision_author == RevisionAuthor.id,
7028.1.1 by Tim Penhey
Speed up the query that is used to count the number of new revisions shown on the +code-index and +branches pages for products.
420
            revision_time_limit(days),
7381.1.2 by Paul Hummer
Refactored Revision.getRecentRevisionsForProduct
421
            BranchRevision.revision == Revision.id,
422
            BranchRevision.branch == Branch.id,
423
            Branch.product == product,
8079.2.2 by Tim Penhey
Only look in active branches for recent revisions.
424
            Branch.lifecycle_status.is_in(DEFAULT_BRANCH_STATUS_IN_LISTING),
7675.747.1 by Jeroen Vermeulen
Intermediate state: stormifying BranchRevision.
425
            BranchRevision.revision_id >= revision_subselect)
7381.1.2 by Paul Hummer
Refactored Revision.getRecentRevisionsForProduct
426
        result_set.config(distinct=True)
427
        return result_set.order_by(Desc(Revision.revision_date))
6736.3.1 by Tim Penhey
Initial work.
428
429
    @staticmethod
11693.1.1 by Stuart Bishop
Fix performance of revision karma allocator under PostgreSQL 8.4
430
    def getRevisionsNeedingKarmaAllocated(limit=None):
6623.4.15 by Tim Penhey
Method to get the revisions needing karma allocated.
431
        """See `IRevisionSet`."""
432
        # Here to stop circular imports.
8138.1.2 by Jonathan Lange
Run migrater over lp.code. Many tests broken and imports failing.
433
        from lp.code.model.branch import Branch
434
        from lp.code.model.branchrevision import BranchRevision
6623.4.15 by Tim Penhey
Method to get the revisions needing karma allocated.
435
11693.1.1 by Stuart Bishop
Fix performance of revision karma allocator under PostgreSQL 8.4
436
        store = IStore(Revision)
437
        results_with_dupes = store.find(
6623.4.15 by Tim Penhey
Method to get the revisions needing karma allocated.
438
            Revision,
439
            Revision.revision_author == RevisionAuthor.id,
6623.4.23 by Tim Penhey
Fix a bug with attempting to allocate karma to an inactive person, and fix logging in the cron script.
440
            RevisionAuthor.person == ValidPersonCache.id,
6623.4.16 by Tim Penhey
Add a new cronscript to be run each night, and remove the creation of the historical karma revisions when a user validates an email address. This is now handled by the cronscript.
441
            Not(Revision.karma_allocated),
11693.1.1 by Stuart Bishop
Fix performance of revision karma allocator under PostgreSQL 8.4
442
            BranchRevision.revision == Revision.id,
443
            BranchRevision.branch == Branch.id,
444
            Or(Branch.product != None, Branch.distroseries != None))[:limit]
445
        # Eliminate duplicate rows, returning <= limit rows
446
        return store.find(
447
            Revision, Revision.id.is_in(
448
                results_with_dupes.get_select_expr(Revision.id)))
6623.4.15 by Tim Penhey
Method to get the revisions needing karma allocated.
449
450
    @staticmethod
6950.2.1 by Tim Penhey
Update the revision getting methods to be date bound to speed up queries.
451
    def getPublicRevisionsForPerson(person, day_limit=30):
6736.3.1 by Tim Penhey
Initial work.
452
        """See `IRevisionSet`."""
6736.3.4 by Tim Penhey
Added tests for addition Revision and RevisionSet methods.
453
        # Here to stop circular imports.
8138.1.2 by Jonathan Lange
Run migrater over lp.code. Many tests broken and imports failing.
454
        from lp.code.model.branch import Branch
455
        from lp.code.model.branchrevision import BranchRevision
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
456
        from lp.registry.model.teammembership import (
6736.3.4 by Tim Penhey
Added tests for addition Revision and RevisionSet methods.
457
            TeamParticipation)
458
6736.3.1 by Tim Penhey
Initial work.
459
        store = Store.of(person)
460
7712.1.1 by Edwin Grubbs
Optimized queries.
461
        origin = [
462
            Revision,
463
            Join(BranchRevision, BranchRevision.revision == Revision.id),
464
            Join(Branch, BranchRevision.branch == Branch.id),
465
            Join(RevisionAuthor,
466
                 Revision.revision_author == RevisionAuthor.id),
467
            ]
468
6736.3.1 by Tim Penhey
Initial work.
469
        if person.is_team:
7712.1.1 by Edwin Grubbs
Optimized queries.
470
            origin.append(
471
                Join(TeamParticipation,
472
                     RevisionAuthor.personID == TeamParticipation.personID))
473
            person_condition = TeamParticipation.team == person
6736.3.1 by Tim Penhey
Initial work.
474
        else:
7712.1.1 by Edwin Grubbs
Optimized queries.
475
            person_condition = RevisionAuthor.person == person
6736.3.1 by Tim Penhey
Initial work.
476
7712.1.1 by Edwin Grubbs
Optimized queries.
477
        result_set = store.using(*origin).find(
6736.3.10 by Tim Penhey
Clean up the query for getting a person's public revisions.
478
            Revision,
7712.1.1 by Edwin Grubbs
Optimized queries.
479
            And(revision_time_limit(day_limit),
480
                person_condition,
13931.4.3 by Ian Booth
Add new transitively_private attribute on branch
481
                Not(Branch.transitively_private)))
7712.1.1 by Edwin Grubbs
Optimized queries.
482
        result_set.config(distinct=True)
6736.3.4 by Tim Penhey
Added tests for addition Revision and RevisionSet methods.
483
        return result_set.order_by(Desc(Revision.revision_date))
6736.3.14 by Tim Penhey
Add getPublicRevisionsForProject to RevisionSet.
484
485
    @staticmethod
7712.1.2 by Edwin Grubbs
Refactored.
486
    def _getPublicRevisionsHelper(obj, day_limit):
10724.1.1 by Henning Eggers
First batch of Project -> ProjectGrpoup renamings.
487
        """Helper method for Products and ProjectGroups."""
6736.3.14 by Tim Penhey
Add getPublicRevisionsForProject to RevisionSet.
488
        # Here to stop circular imports.
8138.1.2 by Jonathan Lange
Run migrater over lp.code. Many tests broken and imports failing.
489
        from lp.code.model.branch import Branch
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
490
        from lp.registry.model.product import Product
8138.1.2 by Jonathan Lange
Run migrater over lp.code. Many tests broken and imports failing.
491
        from lp.code.model.branchrevision import BranchRevision
6736.3.14 by Tim Penhey
Add getPublicRevisionsForProject to RevisionSet.
492
7712.1.1 by Edwin Grubbs
Optimized queries.
493
        origin = [
494
            Revision,
495
            Join(BranchRevision, BranchRevision.revision == Revision.id),
496
            Join(Branch, BranchRevision.branch == Branch.id),
497
            ]
498
13931.4.3 by Ian Booth
Add new transitively_private attribute on branch
499
        conditions = And(revision_time_limit(day_limit),
500
                         Not(Branch.transitively_private))
501
7712.1.2 by Edwin Grubbs
Refactored.
502
        if IProduct.providedBy(obj):
13931.4.3 by Ian Booth
Add new transitively_private attribute on branch
503
            conditions = And(conditions, Branch.product == obj)
10326.1.1 by Henning Eggers
Mechanically renamed IProject* to IProjectGroup*.
504
        elif IProjectGroup.providedBy(obj):
13931.4.3 by Ian Booth
Add new transitively_private attribute on branch
505
            origin.append(Join(Product, Branch.product == Product.id))
506
            conditions = And(conditions, Product.project == obj)
7712.1.2 by Edwin Grubbs
Refactored.
507
        else:
508
            raise AssertionError(
10326.1.3 by Henning Eggers
Fixed too long lines.
509
                "Not an IProduct or IProjectGroup: %r" % obj)
7712.1.2 by Edwin Grubbs
Refactored.
510
511
        result_set = Store.of(obj).using(*origin).find(
512
            Revision, conditions)
7712.1.1 by Edwin Grubbs
Optimized queries.
513
        result_set.config(distinct=True)
6736.3.14 by Tim Penhey
Add getPublicRevisionsForProject to RevisionSet.
514
        return result_set.order_by(Desc(Revision.revision_date))
6736.3.20 by Tim Penhey
Separate out the product and project method calls.
515
7712.1.2 by Edwin Grubbs
Refactored.
516
    @classmethod
517
    def getPublicRevisionsForProduct(cls, product, day_limit=30):
518
        """See `IRevisionSet`."""
519
        return cls._getPublicRevisionsHelper(product, day_limit)
520
521
    @classmethod
10724.1.1 by Henning Eggers
First batch of Project -> ProjectGrpoup renamings.
522
    def getPublicRevisionsForProjectGroup(cls, project, day_limit=30):
7712.1.2 by Edwin Grubbs
Refactored.
523
        """See `IRevisionSet`."""
524
        return cls._getPublicRevisionsHelper(project, day_limit)
525
7675.169.3 by Tim Penhey
Add methods to populate and prune the RevisionCache.
526
    @staticmethod
527
    def updateRevisionCacheForBranch(branch):
528
        """See `IRevisionSet`."""
529
        # Hand crafting the sql insert statement as storm doesn't handle the
530
        # INSERT INTO ... SELECT ... syntax.  Also there is no public api yet
531
        # for storm to get the select statement.
532
533
        # Remove the security proxy to get access to the ID columns.
534
        naked_branch = removeSecurityProxy(branch)
535
536
        insert_columns = ['Revision.id', 'revision_author', 'revision_date']
537
        subselect_clauses = []
538
        if branch.product is None:
539
            insert_columns.append('NULL')
540
            subselect_clauses.append('product IS NULL')
541
        else:
542
            insert_columns.append(str(naked_branch.productID))
543
            subselect_clauses.append('product = %s' % naked_branch.productID)
544
545
        if branch.distroseries is None:
546
            insert_columns.extend(['NULL', 'NULL'])
547
            subselect_clauses.extend(
548
                ['distroseries IS NULL', 'sourcepackagename IS NULL'])
549
        else:
550
            insert_columns.extend(
551
                [str(naked_branch.distroseriesID),
552
                 str(naked_branch.sourcepackagenameID)])
553
            subselect_clauses.extend(
554
                ['distroseries = %s' % naked_branch.distroseriesID,
555
                 'sourcepackagename = %s' % naked_branch.sourcepackagenameID])
556
557
        insert_columns.append(str(branch.private))
558
        if branch.private:
7675.169.7 by Tim Penhey
Updates following review.
559
            subselect_clauses.append('private IS TRUE')
7675.169.3 by Tim Penhey
Add methods to populate and prune the RevisionCache.
560
        else:
7675.169.7 by Tim Penhey
Updates following review.
561
            subselect_clauses.append('private IS FALSE')
7675.169.3 by Tim Penhey
Add methods to populate and prune the RevisionCache.
562
563
        insert_statement = """
564
            INSERT INTO RevisionCache
565
            (revision, revision_author, revision_date,
566
             product, distroseries, sourcepackagename, private)
567
            SELECT %(columns)s FROM Revision
568
            JOIN BranchRevision ON BranchRevision.revision = Revision.id
569
            WHERE Revision.revision_date > (
570
                CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - interval '30 days')
571
            AND BranchRevision.branch = %(branch_id)s
572
            AND Revision.id NOT IN (
573
                SELECT revision FROM RevisionCache
574
                WHERE %(subselect_where)s)
575
            """ % {
576
            'columns': ', '.join(insert_columns),
577
            'branch_id': branch.id,
578
            'subselect_where': ' AND '.join(subselect_clauses),
579
            }
580
        Store.of(branch).execute(insert_statement)
581
582
    @staticmethod
7675.169.6 by Tim Penhey
Fix the pruning.
583
    def pruneRevisionCache(limit):
7675.169.3 by Tim Penhey
Add methods to populate and prune the RevisionCache.
584
        """See `IRevisionSet`."""
7675.169.6 by Tim Penhey
Fix the pruning.
585
        # Storm doesn't handle remove a limited result set:
586
        #    FeatureError: Can't remove a sliced result set
587
        store = IMasterStore(RevisionCache)
588
        epoch = datetime.now(tz=pytz.UTC) - timedelta(days=30)
589
        subquery = Select(
590
            [RevisionCache.id],
591
            RevisionCache.revision_date < epoch,
592
            limit=limit)
593
        store.find(RevisionCache, RevisionCache.id.is_in(subquery)).remove()
7675.169.3 by Tim Penhey
Add methods to populate and prune the RevisionCache.
594
6950.2.2 by Tim Penhey
Factor out the time bound query fragment, and tweak the docstrings.
595
596
def revision_time_limit(day_limit):
597
    """The storm fragment to limit the revision_date field of the Revision."""
598
    now = datetime.now(pytz.UTC)
599
    earliest = now - timedelta(days=day_limit)
600
601
    return And(
602
        Revision.revision_date <= now,
603
        Revision.revision_date > earliest)
7675.169.3 by Tim Penhey
Add methods to populate and prune the RevisionCache.
604
605
606
class RevisionCache(Storm):
607
    """A cached version of a recent revision."""
608
609
    __storm_table__ = 'RevisionCache'
610
611
    id = Int(primary=True)
612
613
    revision_id = Int(name='revision', allow_none=False)
614
    revision = Reference(revision_id, 'Revision.id')
615
7675.169.7 by Tim Penhey
Updates following review.
616
    revision_author_id = Int(name='revision_author', allow_none=False)
7675.169.3 by Tim Penhey
Add methods to populate and prune the RevisionCache.
617
    revision_author = Reference(revision_author_id, 'RevisionAuthor.id')
618
11435.5.1 by Tim Penhey
Extract the branch cloud into its own module.
619
    revision_date = UtcDateTimeCol(notNull=True)
7675.169.3 by Tim Penhey
Add methods to populate and prune the RevisionCache.
620
621
    product_id = Int(name='product', allow_none=True)
622
    product = Reference(product_id, 'Product.id')
623
624
    distroseries_id = Int(name='distroseries', allow_none=True)
625
    distroseries = Reference(distroseries_id, 'DistroSeries.id')
626
627
    sourcepackagename_id = Int(name='sourcepackagename', allow_none=True)
628
    sourcepackagename = Reference(
629
        sourcepackagename_id, 'SourcePackageName.id')
630
631
    private = Bool(allow_none=False, default=False)
7675.169.6 by Tim Penhey
Fix the pruning.
632
633
    def __init__(self, revision):
7675.169.7 by Tim Penhey
Updates following review.
634
        # Make the revision_author assignment first as traversing to the
635
        # revision_author of the revision does a query which causes a store
636
        # flush.  If an assignment has been done already, the RevisionCache
637
        # object would have been implicitly added to the store, and failes
638
        # with an integrity check.
7675.169.6 by Tim Penhey
Fix the pruning.
639
        self.revision_author = revision.revision_author
640
        self.revision = revision
641
        self.revision_date = revision.revision_date