~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# Copyright 2009 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

# pylint: disable-msg=E0211,E0213

"""Team membership interfaces."""

__metaclass__ = type

__all__ = [
    'ACTIVE_STATES',
    'CyclicalTeamMembershipError',
    'DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT',
    'IJoinTeamEvent',
    'ITeamInvitationEvent',
    'ITeamMembership',
    'ITeamMembershipSet',
    'ITeamParticipation',
    'TeamMembershipStatus',
    ]

from lazr.enum import (
    DBEnumeratedType,
    DBItem,
    )
from lazr.restful.declarations import (
    call_with,
    export_as_webservice_entry,
    export_write_operation,
    exported,
    operation_parameters,
    REQUEST_USER,
    )
from lazr.restful.fields import Reference
from lazr.restful.interface import copy_field
from zope.interface import (
    Attribute,
    Interface,
    )
from zope.schema import (
    Bool,
    Choice,
    Datetime,
    Int,
    Text,
    )

from canonical.launchpad import _

# One week before a membership expires we send a notification to the member,
# either inviting him to renew his own membership or asking him to get a team
# admin to do so, depending on the team's renewal policy.
DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT = 7


class TeamMembershipStatus(DBEnumeratedType):
    """TeamMembership Status

    According to the policies specified by each team, the membership status of
    a given member can be one of multiple different statuses. More information
    can be found in the TeamMembership spec.
    """

    PROPOSED = DBItem(1, """
        Proposed

        You are a proposed member of this team. To become an active member
        your subscription has to be approved by one of the team's
        administrators.
        """)

    APPROVED = DBItem(2, """
        Approved

        You are an active member of this team.
        """)

    ADMIN = DBItem(3, """
        Administrator

        You are an administrator of this team.
        """)

    DEACTIVATED = DBItem(4, """
        Deactivated

        Your subscription to this team has been deactivated.
        """)

    EXPIRED = DBItem(5, """
        Expired

        Your subscription to this team is expired.
        """)

    DECLINED = DBItem(6, """
        Declined

        Your proposed subscription to this team has been declined.
        """)

    INVITED = DBItem(7, """
        Invited

        You have been invited as a member of this team. In order to become an
        actual member, you have to accept the invitation.
        """)

    INVITATION_DECLINED = DBItem(8, """
        Invitation declined

        You have been invited as a member of this team but the invitation has
        been declined.
        """)


ACTIVE_STATES = [TeamMembershipStatus.ADMIN, TeamMembershipStatus.APPROVED]


class ITeamMembership(Interface):
    """TeamMembership for Users.

    This table includes *direct* team members only.  Indirect memberships are
    handled by the TeamParticipation table.
    """
    export_as_webservice_entry()

    id = Int(title=_('ID'), required=True, readonly=True)
    team = exported(
        Reference(title=_("Team"), required=True, readonly=True,
                  schema=Interface)) # Specified in interfaces/person.py.
    person = exported(
        Reference(title=_("Member"), required=True, readonly=True,
                  schema=Interface), # Specified in interfaces/person.py.
        exported_as='member')
    proposed_by = Attribute(_('Proponent'))
    reviewed_by = Attribute(
        _("The team admin who approved/rejected the member."))
    acknowledged_by = Attribute(
        _('The person (usually the member or someone acting on his behalf) '
          'that acknowledged (accepted/declined) a membership invitation.'))
    last_changed_by = exported(
        Reference(title=_('Last person who change this'),
                  required=False, readonly=True,
                  schema=Interface)) # Specified in interfaces/person.py.

    datejoined = exported(
        Datetime(title=_("Date joined"), required=False, readonly=True,
                 description=_("The date in which this membership was made "
                               "active for the first time.")),
        exported_as='date_joined')
    dateexpires = exported(
        Datetime(title=_("Date expires"), required=False, readonly=True),
        exported_as='date_expires')
    date_created = Datetime(
        title=_("Date created"), required=False, readonly=True,
        description=_("The date in which this membership was created."))
    date_proposed = Datetime(
        title=_("Date proposed"), required=False, readonly=True,
        description=_("The date in which this membership was proposed."))
    date_acknowledged = Datetime(
        title=_("Date acknowledged"), required=False, readonly=True,
        description=_("The date in which this membership was acknowledged by "
                      "the member (or someone acting on their behalf)."))
    date_reviewed = Datetime(
        title=_("Date reviewed"), required=False, readonly=True,
        description=_("The date in which this membership was approved/"
                      "rejected by one of the team's admins."))
    date_last_changed = Datetime(
        title=_("Date last changed"), required=False, readonly=True,
        description=_("The date in which this membership was last changed."))

    last_change_comment = exported(
        Text(title=_("Comment on the last change"), required=False,
             readonly=True))
    proponent_comment = Text(
        title=_("Proponent comment"), required=False, readonly=True)
    acknowledger_comment = Text(
        title=_("Acknowledger comment"), required=False, readonly=True)
    reviewer_comment = Text(
        title=_("Reviewer comment"), required=False, readonly=True)
    status = exported(
        Choice(title=_("The state of this membership"), required=True,
               readonly=True, vocabulary=TeamMembershipStatus))

    def isExpired():
        """Return True if this membership's status is EXPIRED."""

    def canChangeExpirationDate(person):
        """Can the given person change this membership's expiration date?

        A membership's expiration date can be changed by the team owner, by a
        Launchpad admin or by a team admin. In the latter case, though, the
        expiration date can only be changed if the admin is not changing his
        own membership.
        """

    @call_with(user=REQUEST_USER)
    @operation_parameters(date=copy_field(dateexpires))
    @export_write_operation()
    def setExpirationDate(date, user):
        """Set this membership's expiration date.

        The given date must be None or in the future and the given user must
        be allowed to change this membership's expiration date as per the
        rules defined in canChangeExpirationDate().
        """

    def canBeRenewedByMember():
        """Can this membership be renewed by the member himself?

        A membership can be renewed if the team's renewal policy is ONDEMAND,
        the membership itself is active (status = [ADMIN|APPROVED]) and it's
        set to expire in less than DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT
        days.
        """

    def sendSelfRenewalNotification():
        """Send an email to the team admins notifying that this membership
        has been renewed by the member himself.

        This method must not be called if the team's renewal policy is not
        ONDEMAND.
        """

    def sendAutoRenewalNotification():
        """Send an email to the member and to team admins notifying that this
        membership has been automatically renewed.

        This method must not be called if the team's renewal policy is not
        AUTOMATIC.
        """

    def sendExpirationWarningEmail():
        """Send the member an email warning that the membership will expire.

        This method cannot be called for memberships without an expiration
        date. Emails are not sent to members if their membership has already
        expired or if the member is no longer active.

        :raises AssertionError: if the member has no expiration date of the
            team or if the TeamMembershipRenewalPolicy is AUTOMATIC.
        """

    @call_with(user=REQUEST_USER)
    @operation_parameters(
        status=copy_field(status),
        comment=copy_field(reviewer_comment),
        silent=Bool(title=_("Do not send notifications of status change.  "
                            "For use by Launchpad administrators only."),
                            required=False, default=False))
    @export_write_operation()
    def setStatus(status, user, comment=None, silent=False):
        """Set the status of this membership.

        The user and comment are stored in last_changed_by and
        last_change_comment and may also be stored in proposed_by
        (and proponent_comment), reviewed_by (and reviewer_comment) or
        acknowledged_by (and acknowledger_comment), depending on the state
        transition.

        The given status must be different than the current status.

        Return True if the status got changed, otherwise False.
        """


class ITeamMembershipSet(Interface):
    """A Set for TeamMembership objects."""

    def handleMembershipsExpiringToday(reviewer):
        """Expire or renew the memberships flagged to expire today.

        If the team's renewal policy is AUTOMATIC, renew the membership
        (keeping the same status) and send a notification to the member and
        team admins. Otherwise flag the membership as expired.
        """

    def getMembershipsToExpire(when=None):
        """Return all TeamMemberships that should be expired.

        If when is None, we use datetime.now().

        A TeamMembership should be expired when its expiry date is prior or
        equal to :when: and its status is either ADMIN or APPROVED.
        """

    def new(person, team, status, user, dateexpires=None, comment=None):
        """Create and return a TeamMembership for the given person and team.

        :param status: The TeamMembership's status. Must be one of APPROVED,
            PROPOSED or ADMIN. If the status is APPROVED or ADMIN, this method
            will also take care of filling the TeamParticipation table.
        :param user: The person whose action triggered this membership's
            creation.
        :param dateexpires: The date in which the membership should expire.
        :param comment: The rationale for this membership's creation.
        """

    def getByPersonAndTeam(person, team):
        """Return the TeamMembership object for the given person and team.

        If the given person or team is None, there will obviously be no
        TeamMembership and I'll return None.
        """

    def deactivateActiveMemberships(team, comment, reviewer):
        """Deactivate all team members in ACTIVE_STATES.

        This is a convenience method used before teams are deleted.

        :param team: The team to deactivate.
        :param comment: An explanation for the deactivation.
        :param reviewer: The user doing the deactivation.
        """


class ITeamParticipation(Interface):
    """A TeamParticipation.

    A TeamParticipation object represents a person being a member of a team.
    Please note that because a team is also a person in Launchpad, we can
    have a TeamParticipation object representing a team that is a member of
    another team. We can also have an object that represents a person being a
    member of itself.
    """

    id = Int(title=_('ID'), required=True, readonly=True)
    team = Reference(
        title=_("The team"), required=True, readonly=True,
        schema=Interface) # Specified in interfaces/person.py.
    person = Reference(
        title=_("The member"), required=True, readonly=True,
        schema=Interface) # Specified in interfaces/person.py.


class CyclicalTeamMembershipError(Exception):
    """A change resulting in a team membership cycle was attempted.

    Two teams cannot be members of each other and there cannot be
    any cyclical relationships.  So if A is a member of B and B is
    a member of C then attempting to make C a member of A will
    result in this error being raised.
    """


class IJoinTeamEvent(Interface):
    """A person/team joined (or tried to join) a team."""

    person = Attribute("The person/team who joined the team.")
    team = Attribute("The team.")


class ITeamInvitationEvent(Interface):
    """A new person/team has been invited to a team."""

    member = Attribute("The person/team who was invited.")
    team = Attribute("The team.")