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
|
# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
__all__ = [
'TeamMembershipBreadcrumb',
'TeamInvitationsView',
'TeamMembershipEditView',
]
from datetime import datetime
import pytz
from zope.app.form import CustomWidgetFactory
from zope.app.form.interfaces import InputErrors
from zope.formlib import form
from zope.schema import Date
from lp import _
from lp.app.errors import UnexpectedFormData
from lp.app.widgets.date import DateWidget
from lp.registry.interfaces.teammembership import TeamMembershipStatus
from lp.services.webapp import (
canonical_url,
LaunchpadView,
)
from lp.services.webapp.breadcrumb import Breadcrumb
class TeamMembershipBreadcrumb(Breadcrumb):
"""Builds a breadcrumb for an `ITeamMembership`."""
@property
def text(self):
return "%s's membership" % self.context.person.displayname
class TeamMembershipEditView(LaunchpadView):
def __init__(self, context, request):
super(TeamMembershipEditView, self).__init__(context, request)
self.errormessage = ""
self.prefix = 'membership'
self.max_year = 2050
fields = form.Fields(Date(
__name__='expirationdate', title=_('Expiration date')))
expiration_field = fields['expirationdate']
expiration_field.custom_widget = CustomWidgetFactory(DateWidget)
expires = self.context.dateexpires
UTC = pytz.timezone('UTC')
if self.isExpired():
# For expired members, we will present the team's default
# renewal date.
expires = self.context.team.defaultrenewedexpirationdate
if self.isDeactivated():
# For members who were deactivated, we present by default
# their original expiration date, or, if that has passed, or
# never set, the team's default renewal date.
if expires is None or expires < datetime.now(UTC):
expires = self.context.team.defaultrenewedexpirationdate
if expires is not None:
# We get a datetime from the database, but we want to use a
# datepicker so we must feed it a plain date without time.
expires = expires.date()
data = {'expirationdate': expires}
self.widgets = form.setUpWidgets(
fields, self.prefix, context, request, ignore_request=False,
data=data)
self.expiration_widget = self.widgets['expirationdate']
# Set the acceptable date range for expiration.
self.expiration_widget.from_date = datetime.now(UTC).date()
# Disable the date widget if there is no current or required
# expiration
if not expires:
self.expiration_widget.disabled = True
@property
def label(self):
# This reproduces the logic of the old H1's in the pre-3.0 UI view.
if self.isActive():
prefix = 'Active'
elif self.isInactive():
prefix = 'Inactive'
elif self.isProposed():
prefix = 'Proposed'
elif self.isDeclined():
prefix = 'Declined'
elif self.isInvited() or self.isInvitationDeclined():
prefix = 'Invited'
else:
raise AssertionError('status unknown')
return '%s member %s' % (prefix, self.context.person.displayname)
# Boolean helpers
def isActive(self):
return self.context.status in [TeamMembershipStatus.APPROVED,
TeamMembershipStatus.ADMIN]
def isInactive(self):
return self.context.status in [TeamMembershipStatus.EXPIRED,
TeamMembershipStatus.DEACTIVATED]
def isAdmin(self):
return self.context.status == TeamMembershipStatus.ADMIN
def isProposed(self):
return self.context.status == TeamMembershipStatus.PROPOSED
def isDeclined(self):
return self.context.status == TeamMembershipStatus.DECLINED
def isExpired(self):
return self.context.status == TeamMembershipStatus.EXPIRED
def isDeactivated(self):
return self.context.status == TeamMembershipStatus.DEACTIVATED
def isInvited(self):
return self.context.status == TeamMembershipStatus.INVITED
def isInvitationDeclined(self):
return self.context.status == TeamMembershipStatus.INVITATION_DECLINED
def adminIsSelected(self):
"""Whether the admin radiobutton should be selected."""
request_admin = self.request.get('admin')
if request_admin == 'yes':
return 'checked'
if self.isAdmin():
return 'checked'
return None
def adminIsNotSelected(self):
"""Whether the not-admin radiobutton should be selected."""
if self.adminIsSelected() != 'checked':
return 'checked'
return None
def expiresIsSelected(self):
"""Whether the expiration date radiobutton should be selected."""
request_expires = self.request.get('expires')
if request_expires == 'date':
return 'checked'
if self.isExpired():
# Never checked when expired, because there's another
# radiobutton in that situation.
return None
if self.membershipExpires():
return 'checked'
return None
def neverExpiresIsSelected(self):
"""Whether the never-expires radiobutton should be selected."""
request_expires = self.request.get('expires')
if request_expires == 'never':
return 'checked'
if self.isExpired():
# Never checked when expired, because there's another
# radiobutton in that situation.
return None
if not self.membershipExpires():
return 'checked'
return None
def canChangeExpirationDate(self):
"""Return True if the logged in user can change the expiration date of
this membership.
Team administrators can't change the expiration date of their own
membership.
"""
return self.context.canChangeExpirationDate(self.user)
def membershipExpires(self):
"""Return True if this membership is scheduled to expire one day."""
if self.context.dateexpires is None:
return False
else:
return True
#
# Form post handlers and helpers
#
def processForm(self):
if self.request.method != 'POST':
return
if self.request.form.get('editactive'):
self.processActiveMember()
elif self.request.form.get('editproposed'):
self.processProposedMember()
elif self.request.form.get('editinactive'):
self.processInactiveMember()
def processActiveMember(self):
# This method checks the current status to ensure that we don't
# crash because of users reposting a form.
form = self.request.form
context = self.context
if form.get('deactivate'):
if self.context.status == TeamMembershipStatus.DEACTIVATED:
# This branch and redirect is necessary because
# TeamMembership.setStatus() does not allow us to set an
# already-deactivated account to deactivated, causing
# double form posts to crash there. We instead manually
# ensure that the double-post is harmless.
self.request.response.redirect(
'%s/+members' % canonical_url(context.team))
return
new_status = TeamMembershipStatus.DEACTIVATED
elif form.get('change'):
if (form.get('admin') == "no" and
context.status == TeamMembershipStatus.ADMIN):
new_status = TeamMembershipStatus.APPROVED
elif (form.get('admin') == "yes" and
context.status == TeamMembershipStatus.APPROVED):
new_status = TeamMembershipStatus.ADMIN
else:
# No status change will happen
new_status = self.context.status
else:
raise UnexpectedFormData(
"None of the expected actions were found.")
if self._setMembershipData(new_status):
self.request.response.redirect(
'%s/+members' % canonical_url(context.team))
def processProposedMember(self):
if self.context.status != TeamMembershipStatus.PROPOSED:
# Catch a double-form-post.
self.errormessage = _(
'The membership request for %s has already been processed.' %
self.context.person.displayname)
return
assert self.context.status == TeamMembershipStatus.PROPOSED
if self.request.form.get('decline'):
status = TeamMembershipStatus.DECLINED
elif self.request.form.get('approve'):
status = TeamMembershipStatus.APPROVED
else:
raise UnexpectedFormData(
"None of the expected actions were found.")
if self._setMembershipData(status):
self.request.response.redirect(
'%s/+members' % canonical_url(self.context.team))
def processInactiveMember(self):
if self.context.status not in (TeamMembershipStatus.EXPIRED,
TeamMembershipStatus.DEACTIVATED):
# Catch a double-form-post.
self.errormessage = _(
'The membership request for %s has already been processed.' %
self.context.person.displayname)
return
if self._setMembershipData(TeamMembershipStatus.APPROVED):
self.request.response.redirect(
'%s/+members' % canonical_url(self.context.team))
def _setMembershipData(self, status):
"""Set all data specified on the form, for this TeamMembership.
Get all data from the form, together with the given status and set
them for this TeamMembership object.
Returns True if we successfully set the data, False otherwise.
Callsites should not commit the transaction if we return False.
"""
if self.canChangeExpirationDate():
if self.request.form.get('expires') == 'never':
expires = None
else:
try:
expires = self._getExpirationDate()
except ValueError, err:
self.errormessage = (
'Invalid expiration: %s' % err)
return False
else:
expires = self.context.dateexpires
silent = self.request.form.get('silent', False)
self.context.setExpirationDate(expires, self.user)
self.context.setStatus(
status, self.user, self.request.form_ng.getOne('comment'),
silent)
return True
def _getExpirationDate(self):
"""Return a datetime with the expiration date selected on the form.
Raises ValueError if the date selected is invalid. The use of
that exception is unusual but allows us to present a consistent
API to the caller, who needs to check only for that specific
exception.
"""
expires = None
try:
expires = self.expiration_widget.getInputValue()
except InputErrors, value:
# Handle conversion errors. We have to do this explicitly here
# because we are not using the full form machinery which would
# put the relevant error message into the field error. We are
# mixing the zope3 widget stuff with a hand-crafted form
# processor, so we need to trap this manually.
raise ValueError(value.doc())
if expires is None:
return None
# We used a date picker, so we have a date. What we want is a
# datetime in UTC
UTC = pytz.timezone('UTC')
expires = datetime(expires.year, expires.month, expires.day,
tzinfo=UTC)
return expires
class TeamInvitationsView(LaunchpadView):
"""View for ~team/+invitations."""
@property
def label(self):
return 'Invitations for ' + self.context.displayname
|