~launchpad-pqm/launchpad/devel

13677.4.1 by Danilo Segan
Refactor getNonActiveSubscribers into WITH query.
1
# Copyright 2009-2011 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).
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
3
4
"""Database class for table ArchiveSubscriber."""
5
6
__metaclass__ = type
7
8
__all__ = [
7862.1.5 by Michael Nelson
Reverting some unitentional changes (different quotes, extra commas) from diff.
9
    'ArchiveSubscriber',
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
10
    ]
11
12
import pytz
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
13
from storm.expr import (
14
    And,
15
    Desc,
13677.4.1 by Danilo Segan
Refactor getNonActiveSubscribers into WITH query.
16
    Join,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
17
    LeftJoin,
18
    )
19
from storm.locals import (
20
    DateTime,
21
    Int,
22
    Reference,
23
    Store,
24
    Storm,
25
    Unicode,
26
    )
8546.1.1 by Julian Edwards
IArchiveSubscriber.getNonActiveSubscribers() now behaves properly when the subscriber is not a team.
27
from storm.store import EmptyResultSet
8546.1.3 by Julian Edwards
noodles' review comments
28
from zope.component import getUtility
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
29
from zope.interface import implements
30
7533.2.6 by Julian Edwards
Add doctest
31
from canonical.database.constants import UTC_NOW
32
from canonical.database.enumcol import DBEnum
7675.759.17 by Brad Crittenden
Reverted removal of validate_person.
33
from lp.registry.interfaces.person import validate_person
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
34
from lp.registry.model.teammembership import TeamParticipation
14550.1.1 by Steve Kowalik
Run format-imports over lib/lp and lib/canonical/launchpad
35
from lp.services.identity.interfaces.emailaddress import EmailAddressStatus
36
from lp.services.identity.model.emailaddress import EmailAddress
37
from lp.soyuz.enums import ArchiveSubscriberStatus
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
38
from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthTokenSet
14550.1.1 by Steve Kowalik
Run format-imports over lib/lp and lib/canonical/launchpad
39
from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriber
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
40
from lp.soyuz.model.archiveauthtoken import ArchiveAuthToken
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
41
42
43
class ArchiveSubscriber(Storm):
44
    """See `IArchiveSubscriber`."""
45
    implements(IArchiveSubscriber)
46
    __storm_table__ = 'ArchiveSubscriber'
47
48
    id = Int(primary=True)
49
7844.4.7 by Michael Nelson
Added the ability to have ArchiveAuthTokens also returned when calling IArchiveSubscriber.getBySubscriber().
50
    archive_id = Int(name='archive', allow_none=False)
51
    archive = Reference(archive_id, 'Archive.id')
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
52
7844.4.7 by Michael Nelson
Added the ability to have ArchiveAuthTokens also returned when calling IArchiveSubscriber.getBySubscriber().
53
    registrant_id = Int(name='registrant', allow_none=False)
54
    registrant = Reference(registrant_id, 'Person.id')
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
55
56
    date_created = DateTime(
7533.2.9 by Julian Edwards
Review suggestions from Brad
57
        name='date_created', allow_none=False, tzinfo=pytz.UTC)
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
58
8369.2.8 by Brad Crittenden
Allow private teams to own and subscribe to private PPAs
59
    subscriber_id = Int(
7675.759.17 by Brad Crittenden
Reverted removal of validate_person.
60
        name='subscriber', allow_none=False,
61
        validator=validate_person)
7844.4.7 by Michael Nelson
Added the ability to have ArchiveAuthTokens also returned when calling IArchiveSubscriber.getBySubscriber().
62
    subscriber = Reference(subscriber_id, 'Person.id')
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
63
64
    date_expires = DateTime(
7533.2.9 by Julian Edwards
Review suggestions from Brad
65
        name='date_expires', allow_none=True, tzinfo=pytz.UTC)
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
66
7533.2.6 by Julian Edwards
Add doctest
67
    status = DBEnum(
68
        name='status', allow_none=False,
69
        enum=ArchiveSubscriberStatus)
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
70
71
    description = Unicode(name='description', allow_none=True)
72
73
    date_cancelled = DateTime(
7533.2.9 by Julian Edwards
Review suggestions from Brad
74
        name='date_cancelled', allow_none=True, tzinfo=pytz.UTC)
7533.2.1 by Julian Edwards
ArchiveSubscriber content class, first attempt, no tests yet.
75
7844.4.7 by Michael Nelson
Added the ability to have ArchiveAuthTokens also returned when calling IArchiveSubscriber.getBySubscriber().
76
    cancelled_by_id = Int(name='cancelled_by', allow_none=True)
77
    cancelled_by = Reference(cancelled_by_id, 'Person.id')
7533.2.3 by Julian Edwards
Add IArchive.newSubscription, ArchiveSubscriberSet, zcml and security wrappers.
78
7941.3.2 by Michael Nelson
Added IArchiveSubscriber.title.
79
    @property
8072.4.1 by Michael Nelson
Split previous branch, including only the browser code and 1 necessary new template here.
80
    def displayname(self):
7941.3.2 by Michael Nelson
Added IArchiveSubscriber.title.
81
        """See `IArchiveSubscriber`."""
8831.2.1 by Michael Nelson
Initial wording changes for mpt.
82
        return "%s's access to %s" % (
7976.2.4 by Celso Providelo
fixing new code.
83
            self.subscriber.displayname, self.archive.displayname)
7941.3.2 by Michael Nelson
Added IArchiveSubscriber.title.
84
7533.2.5 by Julian Edwards
Add cancellation method
85
    def cancel(self, cancelled_by):
86
        """See `IArchiveSubscriber`."""
87
        self.date_cancelled = UTC_NOW
88
        self.cancelled_by = cancelled_by
7533.2.6 by Julian Edwards
Add doctest
89
        self.status = ArchiveSubscriberStatus.CANCELLED
7533.2.5 by Julian Edwards
Add cancellation method
90
8137.17.24 by Barry Warsaw
thread merge
91
    def getNonActiveSubscribers(self):
92
        """See `IArchiveSubscriber`."""
93
        # Imported here because of circular imports.
94
        from lp.registry.model.person import Person
95
96
        store = Store.of(self)
97
        if self.subscriber.is_team:
98
13677.4.1 by Danilo Segan
Refactor getNonActiveSubscribers into WITH query.
99
            # We get all the people who already have active tokens for
100
            # this archive (for example, through separate subscriptions).
13677.4.2 by Danilo Segan
Refactor it some more to use LEFT JOIN instead.
101
            auth_token = LeftJoin(
102
                ArchiveAuthToken,
103
                And(ArchiveAuthToken.person_id == Person.id,
104
                    ArchiveAuthToken.archive_id == self.archive_id,
105
                    ArchiveAuthToken.date_deactivated == None))
106
107
            team_participation = Join(
108
                TeamParticipation,
109
                TeamParticipation.personID == Person.id)
110
13677.4.3 by Danilo Segan
Make getNonActiveSubscribers return preferred email addresses as well to cut down on the individual queries for each of them.
111
            # Only return people with preferred email address set.
112
            preferred_email = Join(
113
                EmailAddress, EmailAddress.personID == Person.id)
114
8137.17.24 by Barry Warsaw
thread merge
115
            # We want to get all participants who are themselves
116
            # individuals, not teams:
13677.4.2 by Danilo Segan
Refactor it some more to use LEFT JOIN instead.
117
            non_active_subscribers = store.using(
13677.4.3 by Danilo Segan
Make getNonActiveSubscribers return preferred email addresses as well to cut down on the individual queries for each of them.
118
                Person, team_participation, preferred_email, auth_token).find(
119
                (Person, EmailAddress),
120
                EmailAddress.status == EmailAddressStatus.PREFERRED,
8137.17.24 by Barry Warsaw
thread merge
121
                TeamParticipation.teamID == self.subscriber_id,
13677.4.1 by Danilo Segan
Refactor getNonActiveSubscribers into WITH query.
122
                Person.teamowner == None,
13677.4.3 by Danilo Segan
Make getNonActiveSubscribers return preferred email addresses as well to cut down on the individual queries for each of them.
123
                # There is no existing archive auth token.
13677.4.2 by Danilo Segan
Refactor it some more to use LEFT JOIN instead.
124
                ArchiveAuthToken.person_id == None)
8137.17.24 by Barry Warsaw
thread merge
125
            non_active_subscribers.order_by(Person.name)
126
            return non_active_subscribers
127
        else:
8546.1.2 by Julian Edwards
Replace a count() with a .any() which is a bit more storm-like.
128
            # Subscriber is not a team.
8546.1.3 by Julian Edwards
noodles' review comments
129
            token_set = getUtility(IArchiveAuthTokenSet)
130
            if token_set.getActiveTokenForArchiveAndPerson(
8546.1.4 by Julian Edwards
compare explicitly to None as per guidelines
131
                self.archive, self.subscriber) is not None:
8546.1.1 by Julian Edwards
IArchiveSubscriber.getNonActiveSubscribers() now behaves properly when the subscriber is not a team.
132
                # There are active tokens, so return an empty result
133
                # set.
134
                return EmptyResultSet()
135
136
            # Otherwise return a result set containing only the
13677.4.3 by Danilo Segan
Make getNonActiveSubscribers return preferred email addresses as well to cut down on the individual queries for each of them.
137
            # subscriber and their preferred email address.
138
            return store.find(
139
                (Person, EmailAddress),
140
                Person.id == self.subscriber_id,
141
                EmailAddress.personID == Person.id,
142
                EmailAddress.status == EmailAddressStatus.PREFERRED)
8137.17.24 by Barry Warsaw
thread merge
143
7533.2.3 by Julian Edwards
Add IArchive.newSubscription, ArchiveSubscriberSet, zcml and security wrappers.
144
145
class ArchiveSubscriberSet:
146
    """See `IArchiveSubscriberSet`."""
147
12965.4.2 by Abel Deuring
ArchiveSubscriberSet.getBySubscriber() and ArchiveSubscriberSet.getBySubscriberWithActiveToken() refactored
148
    def _getBySubscriber(self, subscriber, archive, current_only,
149
                         with_active_tokens):
150
        """Return all the subscriptions for a person.
7844.4.9 by Michael Nelson
Factored out getBySubscriberWithTokens() and created private methods to keep the queries DRY.
151
12965.4.2 by Abel Deuring
ArchiveSubscriberSet.getBySubscriber() and ArchiveSubscriberSet.getBySubscriberWithActiveToken() refactored
152
        :param subscriber: An `IPerson` for whom to return all
153
            `ArchiveSubscriber` records.
154
        :param archive: An optional `IArchive` which restricts
155
            the results to that particular archive.
156
        :param current_only: Whether the result should only include current
157
            subscriptions (which is the default).
158
        :param with_active_tokens: Indicates whether the tokens for the given
159
            subscribers subscriptions should be included in the resultset.
160
            By default the tokens are not included in the resultset.
161
^       """
7844.4.9 by Michael Nelson
Factored out getBySubscriberWithTokens() and created private methods to keep the queries DRY.
162
        # Grab the extra Storm expressions, for this query,
163
        # depending on the params:
164
        extra_exprs = self._getExprsForSubscriptionQueries(
165
            archive, current_only)
12965.4.2 by Abel Deuring
ArchiveSubscriberSet.getBySubscriber() and ArchiveSubscriberSet.getBySubscriberWithActiveToken() refactored
166
        origin = [
167
            ArchiveSubscriber,
168
            Join(
169
                TeamParticipation,
170
                TeamParticipation.teamID == ArchiveSubscriber.subscriber_id)]
171
172
        if with_active_tokens:
173
            result_row = (ArchiveSubscriber, ArchiveAuthToken)
174
            # We need a left join with ArchiveSubscriber as
175
            # the origin:
176
            origin.append(
177
                LeftJoin(
178
                    ArchiveAuthToken,
179
                    And(
180
                        ArchiveAuthToken.archive_id ==
181
                            ArchiveSubscriber.archive_id,
182
                        ArchiveAuthToken.person_id == subscriber.id,
183
                        ArchiveAuthToken.date_deactivated == None)))
184
        else:
185
            result_row = ArchiveSubscriber
7844.4.9 by Michael Nelson
Factored out getBySubscriberWithTokens() and created private methods to keep the queries DRY.
186
187
        # Set the main expression to find all the subscriptions for
188
        # which the subscriber is a direct subscriber OR is a member
189
        # of a subscribed team.
190
        # Note: the subscription to the owner itself will also be
191
        # part of the join as there is a TeamParticipation entry
192
        # showing that each person is a member of the "team" that
193
        # consists of themselves.
194
        store = Store.of(subscriber)
12965.4.2 by Abel Deuring
ArchiveSubscriberSet.getBySubscriber() and ArchiveSubscriberSet.getBySubscriberWithActiveToken() refactored
195
        return store.using(*origin).find(
196
            result_row,
12965.4.1 by Abel Deuring
remove a pointless inefficient subquery in ArchiveSubscriberSet.getBySubscriber() and ArchiveSubscriberSet.getBySubscriberWithActiveToken()
197
            TeamParticipation.personID == subscriber.id,
7844.4.9 by Michael Nelson
Factored out getBySubscriberWithTokens() and created private methods to keep the queries DRY.
198
            *extra_exprs).order_by(Desc(ArchiveSubscriber.date_created))
199
12965.4.2 by Abel Deuring
ArchiveSubscriberSet.getBySubscriber() and ArchiveSubscriberSet.getBySubscriberWithActiveToken() refactored
200
    def getBySubscriber(self, subscriber, archive=None, current_only=True):
201
        """See `IArchiveSubscriberSet`."""
202
        return self._getBySubscriber(subscriber, archive, current_only, False)
203
7862.1.6 by Michael Nelson
Changes after al-maisan's review. Mainly clarifying the names of methods.
204
    def getBySubscriberWithActiveToken(self, subscriber, archive=None):
7844.4.9 by Michael Nelson
Factored out getBySubscriberWithTokens() and created private methods to keep the queries DRY.
205
        """See `IArchiveSubscriberSet`."""
12965.4.2 by Abel Deuring
ArchiveSubscriberSet.getBySubscriber() and ArchiveSubscriberSet.getBySubscriberWithActiveToken() refactored
206
        return self._getBySubscriber(subscriber, archive, True, True)
7844.4.9 by Michael Nelson
Factored out getBySubscriberWithTokens() and created private methods to keep the queries DRY.
207
208
    def getByArchive(self, archive, current_only=True):
209
        """See `IArchiveSubscriberSet`."""
210
        # Grab the extra Storm expressions, for this query,
211
        # depending on the params:
212
        extra_exprs = self._getExprsForSubscriptionQueries(
213
            archive, current_only)
214
215
        store = Store.of(archive)
216
        return store.find(
217
            ArchiveSubscriber,
218
            *extra_exprs).order_by(Desc(ArchiveSubscriber.date_created))
219
220
    def _getExprsForSubscriptionQueries(self, archive=None,
221
                                        current_only=True):
222
        """Return the Storm expressions required for the parameters.
223
224
        Just to keep the code DRY.
225
        """
7660.9.4 by Michael Nelson
Changes after Celso's review.
226
        extra_exprs = []
7844.4.7 by Michael Nelson
Added the ability to have ArchiveAuthTokens also returned when calling IArchiveSubscriber.getBySubscriber().
227
228
        # Restrict the results to the specified archive if requested:
7660.9.4 by Michael Nelson
Changes after Celso's review.
229
        if archive:
230
            extra_exprs.append(ArchiveSubscriber.archive == archive)
231
7844.4.7 by Michael Nelson
Added the ability to have ArchiveAuthTokens also returned when calling IArchiveSubscriber.getBySubscriber().
232
        # Restrict the results to only those subscriptions that are current
233
        # if requested:
7844.4.5 by Michael Nelson
Renamed the ArchiveSubscriberStatus.ACTIVE to CURRENT.
234
        if current_only:
7844.4.3 by Michael Nelson
Added default active_only param to IArchiveSubscriberSet.getBySubscriber().
235
            extra_exprs.append(
7844.4.5 by Michael Nelson
Renamed the ArchiveSubscriberStatus.ACTIVE to CURRENT.
236
                ArchiveSubscriber.status == ArchiveSubscriberStatus.CURRENT)
7844.4.3 by Michael Nelson
Added default active_only param to IArchiveSubscriberSet.getBySubscriber().
237
7844.4.9 by Michael Nelson
Factored out getBySubscriberWithTokens() and created private methods to keep the queries DRY.
238
        return extra_exprs