~launchpad-pqm/launchpad/devel

8687.15.17 by Karl Fogel
Add the copyright header block to the rest of the files under lib/lp/.
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4983.1.1 by Curtis Hovey
Added lint exceptions to __init__.py and interface/*.py.
4
# pylint: disable-msg=E0211,E0213
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
5
6
"""Team membership interfaces."""
7
8
__metaclass__ = type
9
5723.6.1 by Barry Warsaw
Move TeamMembershipStatus into the interface module where it belongs.
10
__all__ = [
12017.1.7 by j.c.sackett
Moved ACTIVE_STATES into TeamMembership interfaces and had it imported in the places its used.
11
    'ACTIVE_STATES',
6220.2.1 by Guilherme Salgado
Fix the bug.
12
    'CyclicalTeamMembershipError',
5723.6.1 by Barry Warsaw
Move TeamMembershipStatus into the interface module where it belongs.
13
    'DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT',
13130.1.14 by Curtis Hovey
Moved launchpad.event into registry interfaces.
14
    'IJoinTeamEvent',
15
    'ITeamInvitationEvent',
5723.6.1 by Barry Warsaw
Move TeamMembershipStatus into the interface module where it belongs.
16
    'ITeamMembership',
17
    'ITeamMembershipSet',
18
    'ITeamParticipation',
19
    'TeamMembershipStatus',
20
    ]
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
21
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
22
from lazr.enum import (
23
    DBEnumeratedType,
24
    DBItem,
25
    )
26
from lazr.restful.declarations import (
27
    call_with,
28
    export_as_webservice_entry,
29
    export_write_operation,
30
    exported,
31
    operation_parameters,
32
    REQUEST_USER,
33
    )
34
from lazr.restful.fields import Reference
35
from lazr.restful.interface import copy_field
36
from zope.interface import (
37
    Attribute,
38
    Interface,
39
    )
40
from zope.schema import (
41
    Bool,
42
    Choice,
43
    Datetime,
44
    Int,
45
    Text,
46
    )
5723.6.1 by Barry Warsaw
Move TeamMembershipStatus into the interface module where it belongs.
47
14600.1.12 by Curtis Hovey
Move i18n to lp.
48
from lp import _
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
49
4108.4.13 by Guilherme Salgado
Second half of the fix for https://launchpad.net/bugs/70519: Allow members of teams with an ONDEMAND renewal policy to renew their own memberships
50
# One week before a membership expires we send a notification to the member,
51
# either inviting him to renew his own membership or asking him to get a team
52
# admin to do so, depending on the team's renewal policy.
53
DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT = 7
54
10063.2.1 by Jonathan Davies
Added a blank line before UserCannotChangeMembershipSilently() class for coding
55
5723.6.1 by Barry Warsaw
Move TeamMembershipStatus into the interface module where it belongs.
56
class TeamMembershipStatus(DBEnumeratedType):
57
    """TeamMembership Status
58
59
    According to the policies specified by each team, the membership status of
60
    a given member can be one of multiple different statuses. More information
61
    can be found in the TeamMembership spec.
62
    """
63
64
    PROPOSED = DBItem(1, """
65
        Proposed
66
67
        You are a proposed member of this team. To become an active member
68
        your subscription has to be approved by one of the team's
69
        administrators.
70
        """)
71
72
    APPROVED = DBItem(2, """
73
        Approved
74
75
        You are an active member of this team.
76
        """)
77
78
    ADMIN = DBItem(3, """
79
        Administrator
80
81
        You are an administrator of this team.
82
        """)
83
84
    DEACTIVATED = DBItem(4, """
85
        Deactivated
86
87
        Your subscription to this team has been deactivated.
88
        """)
89
90
    EXPIRED = DBItem(5, """
91
        Expired
92
93
        Your subscription to this team is expired.
94
        """)
95
96
    DECLINED = DBItem(6, """
97
        Declined
98
99
        Your proposed subscription to this team has been declined.
100
        """)
101
102
    INVITED = DBItem(7, """
103
        Invited
104
105
        You have been invited as a member of this team. In order to become an
106
        actual member, you have to accept the invitation.
107
        """)
108
109
    INVITATION_DECLINED = DBItem(8, """
110
        Invitation declined
111
112
        You have been invited as a member of this team but the invitation has
113
        been declined.
114
        """)
115
116
12017.1.7 by j.c.sackett
Moved ACTIVE_STATES into TeamMembership interfaces and had it imported in the places its used.
117
ACTIVE_STATES = [TeamMembershipStatus.ADMIN, TeamMembershipStatus.APPROVED]
118
119
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
120
class ITeamMembership(Interface):
12019.11.1 by Brad Crittenden
Rework cleanTeamParticipation. Works but is still inefficient.
121
    """TeamMembership for Users.
122
123
    This table includes *direct* team members only.  Indirect memberships are
124
    handled by the TeamParticipation table.
125
    """
6415.4.19 by Leonard Richardson
Simplified some code.
126
    export_as_webservice_entry()
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
127
128
    id = Int(title=_('ID'), required=True, readonly=True)
6246.3.9 by Guilherme Salgado
Remove rest/teammembership.py and use declarations to publish ITeamMembership.
129
    team = exported(
6945.2.2 by Guilherme Salgado
Change some attributes of ITeamMembership to match their real permissions as defined in zcml.
130
        Reference(title=_("Team"), required=True, readonly=True,
6998.3.3 by Guilherme Salgado
A couple changes after Francis' review
131
                  schema=Interface)) # Specified in interfaces/person.py.
6246.3.9 by Guilherme Salgado
Remove rest/teammembership.py and use declarations to publish ITeamMembership.
132
    person = exported(
6945.2.2 by Guilherme Salgado
Change some attributes of ITeamMembership to match their real permissions as defined in zcml.
133
        Reference(title=_("Member"), required=True, readonly=True,
6998.3.3 by Guilherme Salgado
A couple changes after Francis' review
134
                  schema=Interface), # Specified in interfaces/person.py.
6246.3.9 by Guilherme Salgado
Remove rest/teammembership.py and use declarations to publish ITeamMembership.
135
        exported_as='member')
5825.2.3 by Guilherme Salgado
Fix the damn thing
136
    proposed_by = Attribute(_('Proponent'))
137
    reviewed_by = Attribute(
138
        _("The team admin who approved/rejected the member."))
139
    acknowledged_by = Attribute(
140
        _('The person (usually the member or someone acting on his behalf) '
141
          'that acknowledged (accepted/declined) a membership invitation.'))
6246.3.9 by Guilherme Salgado
Remove rest/teammembership.py and use declarations to publish ITeamMembership.
142
    last_changed_by = exported(
6998.3.3 by Guilherme Salgado
A couple changes after Francis' review
143
        Reference(title=_('Last person who change this'),
144
                  required=False, readonly=True,
145
                  schema=Interface)) # Specified in interfaces/person.py.
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
146
6246.3.9 by Guilherme Salgado
Remove rest/teammembership.py and use declarations to publish ITeamMembership.
147
    datejoined = exported(
148
        Datetime(title=_("Date joined"), required=False, readonly=True,
149
                 description=_("The date in which this membership was made "
150
                               "active for the first time.")),
151
        exported_as='date_joined')
152
    dateexpires = exported(
6945.2.2 by Guilherme Salgado
Change some attributes of ITeamMembership to match their real permissions as defined in zcml.
153
        Datetime(title=_("Date expires"), required=False, readonly=True),
6246.3.9 by Guilherme Salgado
Remove rest/teammembership.py and use declarations to publish ITeamMembership.
154
        exported_as='date_expires')
5825.2.3 by Guilherme Salgado
Fix the damn thing
155
    date_created = Datetime(
156
        title=_("Date created"), required=False, readonly=True,
157
        description=_("The date in which this membership was created."))
158
    date_proposed = Datetime(
5825.2.9 by Guilherme Salgado
Few fixes suggested by Brad/Francis
159
        title=_("Date proposed"), required=False, readonly=True,
5825.2.3 by Guilherme Salgado
Fix the damn thing
160
        description=_("The date in which this membership was proposed."))
161
    date_acknowledged = Datetime(
5825.2.9 by Guilherme Salgado
Few fixes suggested by Brad/Francis
162
        title=_("Date acknowledged"), required=False, readonly=True,
5825.2.3 by Guilherme Salgado
Fix the damn thing
163
        description=_("The date in which this membership was acknowledged by "
164
                      "the member (or someone acting on their behalf)."))
165
    date_reviewed = Datetime(
5825.2.9 by Guilherme Salgado
Few fixes suggested by Brad/Francis
166
        title=_("Date reviewed"), required=False, readonly=True,
5825.2.3 by Guilherme Salgado
Fix the damn thing
167
        description=_("The date in which this membership was approved/"
168
                      "rejected by one of the team's admins."))
169
    date_last_changed = Datetime(
5825.2.9 by Guilherme Salgado
Few fixes suggested by Brad/Francis
170
        title=_("Date last changed"), required=False, readonly=True,
5825.2.3 by Guilherme Salgado
Fix the damn thing
171
        description=_("The date in which this membership was last changed."))
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
172
6246.3.9 by Guilherme Salgado
Remove rest/teammembership.py and use declarations to publish ITeamMembership.
173
    last_change_comment = exported(
174
        Text(title=_("Comment on the last change"), required=False,
175
             readonly=True))
5825.2.3 by Guilherme Salgado
Fix the damn thing
176
    proponent_comment = Text(
5825.2.9 by Guilherme Salgado
Few fixes suggested by Brad/Francis
177
        title=_("Proponent comment"), required=False, readonly=True)
5825.2.3 by Guilherme Salgado
Fix the damn thing
178
    acknowledger_comment = Text(
5825.2.9 by Guilherme Salgado
Few fixes suggested by Brad/Francis
179
        title=_("Acknowledger comment"), required=False, readonly=True)
5825.2.3 by Guilherme Salgado
Fix the damn thing
180
    reviewer_comment = Text(
5825.2.9 by Guilherme Salgado
Few fixes suggested by Brad/Francis
181
        title=_("Reviewer comment"), required=False, readonly=True)
6246.3.9 by Guilherme Salgado
Remove rest/teammembership.py and use declarations to publish ITeamMembership.
182
    status = exported(
183
        Choice(title=_("The state of this membership"), required=True,
184
               readonly=True, vocabulary=TeamMembershipStatus))
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
185
186
    def isExpired():
187
        """Return True if this membership's status is EXPIRED."""
188
3691.9.81 by Guilherme Salgado
Move the logic to prevent team admins from changing the expiration date of their own memberships from browser code to database code
189
    def canChangeExpirationDate(person):
190
        """Can the given person change this membership's expiration date?
4785.3.5 by Jeroen Vermeulen
Removed whitespace at ends of lines.
191
3691.9.81 by Guilherme Salgado
Move the logic to prevent team admins from changing the expiration date of their own memberships from browser code to database code
192
        A membership's expiration date can be changed by the team owner, by a
193
        Launchpad admin or by a team admin. In the latter case, though, the
194
        expiration date can only be changed if the admin is not changing his
195
        own membership.
196
        """
197
7377.1.1 by Guilherme Salgado
Fix the two bugs
198
    @call_with(user=REQUEST_USER)
199
    @operation_parameters(date=copy_field(dateexpires))
200
    @export_write_operation()
3691.9.81 by Guilherme Salgado
Move the logic to prevent team admins from changing the expiration date of their own memberships from browser code to database code
201
    def setExpirationDate(date, user):
202
        """Set this membership's expiration date.
203
204
        The given date must be None or in the future and the given user must
205
        be allowed to change this membership's expiration date as per the
206
        rules defined in canChangeExpirationDate().
207
        """
208
4108.4.13 by Guilherme Salgado
Second half of the fix for https://launchpad.net/bugs/70519: Allow members of teams with an ONDEMAND renewal policy to renew their own memberships
209
    def canBeRenewedByMember():
210
        """Can this membership be renewed by the member himself?
211
212
        A membership can be renewed if the team's renewal policy is ONDEMAND,
213
        the membership itself is active (status = [ADMIN|APPROVED]) and it's
11475.2.2 by Brad Crittenden
Fixed lint issues
214
        set to expire in less than DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT
215
        days.
4108.4.13 by Guilherme Salgado
Second half of the fix for https://launchpad.net/bugs/70519: Allow members of teams with an ONDEMAND renewal policy to renew their own memberships
216
        """
217
218
    def sendSelfRenewalNotification():
219
        """Send an email to the team admins notifying that this membership
220
        has been renewed by the member himself.
221
222
        This method must not be called if the team's renewal policy is not
223
        ONDEMAND.
224
        """
225
4108.4.9 by Guilherme Salgado
Fix the flag-expired-memberships.py cronscript to auto renew memberships of teams which have a renewal policy set to AUTOMATIC
226
    def sendAutoRenewalNotification():
227
        """Send an email to the member and to team admins notifying that this
228
        membership has been automatically renewed.
229
230
        This method must not be called if the team's renewal policy is not
231
        AUTOMATIC.
232
        """
233
3691.272.22 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/70518: Notify team members when their membership is going to expire
234
    def sendExpirationWarningEmail():
12481.1.5 by Curtis Hovey
Updated sendExpirationWarningEmail documentation. Do not assert that the expiration date has past; silently return instead.
235
        """Send the member an email warning that the membership will expire.
236
237
        This method cannot be called for memberships without an expiration
238
        date. Emails are not sent to members if their membership has already
239
        expired or if the member is no longer active.
240
241
        :raises AssertionError: if the member has no expiration date of the
242
            team or if the TeamMembershipRenewalPolicy is AUTOMATIC.
3691.272.22 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/70518: Notify team members when their membership is going to expire
243
        """
244
7377.1.1 by Guilherme Salgado
Fix the two bugs
245
    @call_with(user=REQUEST_USER)
246
    @operation_parameters(
247
        status=copy_field(status),
10054.14.10 by Jonathan Davies
Expose our new silent argument via the API.
248
        comment=copy_field(reviewer_comment),
11475.2.2 by Brad Crittenden
Fixed lint issues
249
        silent=Bool(title=_("Do not send notifications of status change.  "
250
                            "For use by Launchpad administrators only."),
10054.14.10 by Jonathan Davies
Expose our new silent argument via the API.
251
                            required=False, default=False))
7377.1.1 by Guilherme Salgado
Fix the two bugs
252
    @export_write_operation()
10054.14.1 by Jonathan Davies
Added an option to a person's team membership so that changes to their
253
    def setStatus(status, user, comment=None, silent=False):
3691.272.9 by Guilherme Salgado
A lot of refactorings, cleanups and improved tests
254
        """Set the status of this membership.
4785.3.5 by Jeroen Vermeulen
Removed whitespace at ends of lines.
255
7377.1.1 by Guilherme Salgado
Fix the two bugs
256
        The user and comment are stored in last_changed_by and
5825.2.3 by Guilherme Salgado
Fix the damn thing
257
        last_change_comment and may also be stored in proposed_by
258
        (and proponent_comment), reviewed_by (and reviewer_comment) or
259
        acknowledged_by (and acknowledger_comment), depending on the state
260
        transition.
3691.272.9 by Guilherme Salgado
A lot of refactorings, cleanups and improved tests
261
262
        The given status must be different than the current status.
9778.4.2 by Bjorn Tillenius
Make TeamMembership.setStatus return whether the status actually changed.
263
264
        Return True if the status got changed, otherwise False.
2770.1.47 by Guilherme Salgado
New cronscript to flag expired team memberships and some other cleanups.
265
        """
266
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
267
268
class ITeamMembershipSet(Interface):
269
    """A Set for TeamMembership objects."""
270
4108.4.9 by Guilherme Salgado
Fix the flag-expired-memberships.py cronscript to auto renew memberships of teams which have a renewal policy set to AUTOMATIC
271
    def handleMembershipsExpiringToday(reviewer):
272
        """Expire or renew the memberships flagged to expire today.
273
274
        If the team's renewal policy is AUTOMATIC, renew the membership
275
        (keeping the same status) and send a notification to the member and
276
        team admins. Otherwise flag the membership as expired.
277
        """
278
3691.272.22 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/70518: Notify team members when their membership is going to expire
279
    def getMembershipsToExpire(when=None):
2770.1.47 by Guilherme Salgado
New cronscript to flag expired team memberships and some other cleanups.
280
        """Return all TeamMemberships that should be expired.
281
3691.272.22 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/70518: Notify team members when their membership is going to expire
282
        If when is None, we use datetime.now().
283
2770.1.47 by Guilherme Salgado
New cronscript to flag expired team memberships and some other cleanups.
284
        A TeamMembership should be expired when its expiry date is prior or
3691.272.22 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/70518: Notify team members when their membership is going to expire
285
        equal to :when: and its status is either ADMIN or APPROVED.
2770.1.47 by Guilherme Salgado
New cronscript to flag expired team memberships and some other cleanups.
286
        """
287
6988.2.1 by Guilherme Salgado
Publish the very basic bits of IDistribution and IDistributionSet plus a few minor improvements.
288
    def new(person, team, status, user, dateexpires=None, comment=None):
289
        """Create and return a TeamMembership for the given person and team.
2770.1.47 by Guilherme Salgado
New cronscript to flag expired team memberships and some other cleanups.
290
6988.2.1 by Guilherme Salgado
Publish the very basic bits of IDistribution and IDistributionSet plus a few minor improvements.
291
        :param status: The TeamMembership's status. Must be one of APPROVED,
292
            PROPOSED or ADMIN. If the status is APPROVED or ADMIN, this method
293
            will also take care of filling the TeamParticipation table.
294
        :param user: The person whose action triggered this membership's
295
            creation.
296
        :param dateexpires: The date in which the membership should expire.
297
        :param comment: The rationale for this membership's creation.
2770.1.47 by Guilherme Salgado
New cronscript to flag expired team memberships and some other cleanups.
298
        """
299
4108.4.15 by Guilherme Salgado
Bunch of changes suggested by Barry/Francis
300
    def getByPersonAndTeam(person, team):
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
301
        """Return the TeamMembership object for the given person and team.
302
4108.4.15 by Guilherme Salgado
Bunch of changes suggested by Barry/Francis
303
        If the given person or team is None, there will obviously be no
304
        TeamMembership and I'll return None.
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
305
        """
306
12617.1.3 by Curtis Hovey
Moved deactivateAllMembers to ITeamMembershipSet.deactivateActiveMemberships.
307
    def deactivateActiveMemberships(team, comment, reviewer):
308
        """Deactivate all team members in ACTIVE_STATES.
309
310
        This is a convenience method used before teams are deleted.
311
312
        :param team: The team to deactivate.
313
        :param comment: An explanation for the deactivation.
314
        :param reviewer: The user doing the deactivation.
315
        """
316
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
317
318
class ITeamParticipation(Interface):
319
    """A TeamParticipation.
320
321
    A TeamParticipation object represents a person being a member of a team.
322
    Please note that because a team is also a person in Launchpad, we can
323
    have a TeamParticipation object representing a team that is a member of
324
    another team. We can also have an object that represents a person being a
325
    member of itself.
326
    """
327
328
    id = Int(title=_('ID'), required=True, readonly=True)
6945.2.2 by Guilherme Salgado
Change some attributes of ITeamMembership to match their real permissions as defined in zcml.
329
    team = Reference(
6998.3.3 by Guilherme Salgado
A couple changes after Francis' review
330
        title=_("The team"), required=True, readonly=True,
331
        schema=Interface) # Specified in interfaces/person.py.
6945.2.2 by Guilherme Salgado
Change some attributes of ITeamMembership to match their real permissions as defined in zcml.
332
    person = Reference(
6998.3.3 by Guilherme Salgado
A couple changes after Francis' review
333
        title=_("The member"), required=True, readonly=True,
334
        schema=Interface) # Specified in interfaces/person.py.
2908.3.1 by Guilherme Salgado
Get rid of TeamMembershipSubset and move TeamMembership classes from database/person.py to database/teammembership.py
335
6220.2.1 by Guilherme Salgado
Fix the bug.
336
337
class CyclicalTeamMembershipError(Exception):
6220.2.3 by Guilherme Salgado
Couple changes suggested by Barry.
338
    """A change resulting in a team membership cycle was attempted.
339
340
    Two teams cannot be members of each other and there cannot be
341
    any cyclical relationships.  So if A is a member of B and B is
342
    a member of C then attempting to make C a member of A will
343
    result in this error being raised.
11475.2.1 by Brad Crittenden
Catch CyclicalTeamMembershipError.
344
    """
13130.1.14 by Curtis Hovey
Moved launchpad.event into registry interfaces.
345
346
347
class IJoinTeamEvent(Interface):
348
    """A person/team joined (or tried to join) a team."""
349
350
    person = Attribute("The person/team who joined the team.")
351
    team = Attribute("The team.")
352
353
354
class ITeamInvitationEvent(Interface):
355
    """A new person/team has been invited to a team."""
356
357
    member = Attribute("The person/team who was invited.")
358
    team = Attribute("The team.")