root / snf-astakos-app / astakos / im / forms.py @ 892410d3
History | View | Annotate | Download (37.6 kB)
1 |
# Copyright 2011-2012 GRNET S.A. All rights reserved.
|
---|---|
2 |
#
|
3 |
# Redistribution and use in source and binary forms, with or
|
4 |
# without modification, are permitted provided that the following
|
5 |
# conditions are met:
|
6 |
#
|
7 |
# 1. Redistributions of source code must retain the above
|
8 |
# copyright notice, this list of conditions and the following
|
9 |
# disclaimer.
|
10 |
#
|
11 |
# 2. Redistributions in binary form must reproduce the above
|
12 |
# copyright notice, this list of conditions and the following
|
13 |
# disclaimer in the documentation and/or other materials
|
14 |
# provided with the distribution.
|
15 |
#
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
27 |
# POSSIBILITY OF SUCH DAMAGE.
|
28 |
#
|
29 |
# The views and conclusions contained in the software and
|
30 |
# documentation are those of the authors and should not be
|
31 |
# interpreted as representing official policies, either expressed
|
32 |
# or implied, of GRNET S.A.
|
33 |
from urlparse import urljoin |
34 |
from random import random |
35 |
|
36 |
from django import forms |
37 |
from django.utils.translation import ugettext as _ |
38 |
from django.contrib.auth.forms import ( |
39 |
UserCreationForm, AuthenticationForm, |
40 |
PasswordResetForm, PasswordChangeForm, |
41 |
SetPasswordForm) |
42 |
from django.core.mail import send_mail |
43 |
from django.contrib.auth.tokens import default_token_generator |
44 |
from django.template import Context, loader |
45 |
from django.utils.http import int_to_base36 |
46 |
from django.core.urlresolvers import reverse |
47 |
from django.utils.safestring import mark_safe |
48 |
from django.utils.encoding import smart_str |
49 |
from django.conf import settings |
50 |
from django.forms.models import fields_for_model |
51 |
from django.db import transaction |
52 |
from django.utils.encoding import smart_unicode |
53 |
from django.core import validators |
54 |
from django.contrib.auth.models import AnonymousUser |
55 |
|
56 |
from astakos.im.models import ( |
57 |
AstakosUser, EmailChange, Invitation, |
58 |
# AstakosGroup, GroupKind,
|
59 |
Resource, PendingThirdPartyUser, get_latest_terms, RESOURCE_SEPARATOR, |
60 |
ProjectApplication) |
61 |
from astakos.im.settings import ( |
62 |
INVITATIONS_PER_LEVEL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, |
63 |
RECAPTCHA_ENABLED, DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL, |
64 |
PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN, |
65 |
MODERATION_ENABLED) |
66 |
from astakos.im.widgets import DummyWidget, RecaptchaWidget |
67 |
from astakos.im.functions import send_change_email, submit_application |
68 |
|
69 |
from astakos.im.util import reserved_email, get_query |
70 |
from astakos.im import auth_providers |
71 |
|
72 |
import astakos.im.messages as astakos_messages |
73 |
|
74 |
import logging |
75 |
import hashlib |
76 |
import recaptcha.client.captcha as captcha |
77 |
import re |
78 |
|
79 |
logger = logging.getLogger(__name__) |
80 |
|
81 |
DOMAIN_VALUE_REGEX = re.compile( |
82 |
r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
|
83 |
re.IGNORECASE) |
84 |
|
85 |
class StoreUserMixin(object): |
86 |
@transaction.commit_on_success
|
87 |
def store_user(self, user, request): |
88 |
user.save() |
89 |
self.post_store_user(user, request)
|
90 |
return user
|
91 |
|
92 |
def post_store_user(self, user, request): |
93 |
"""
|
94 |
Interface method for descendant backends to be able to do stuff within
|
95 |
the transaction enabled by store_user.
|
96 |
"""
|
97 |
pass
|
98 |
|
99 |
|
100 |
class LocalUserCreationForm(UserCreationForm, StoreUserMixin): |
101 |
"""
|
102 |
Extends the built in UserCreationForm in several ways:
|
103 |
|
104 |
* Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
|
105 |
* The username field isn't visible and it is assigned a generated id.
|
106 |
* User created is not active.
|
107 |
"""
|
108 |
recaptcha_challenge_field = forms.CharField(widget=DummyWidget) |
109 |
recaptcha_response_field = forms.CharField( |
110 |
widget=RecaptchaWidget, label='')
|
111 |
|
112 |
class Meta: |
113 |
model = AstakosUser |
114 |
fields = ("email", "first_name", "last_name", |
115 |
"has_signed_terms", "has_signed_terms") |
116 |
|
117 |
def __init__(self, *args, **kwargs): |
118 |
"""
|
119 |
Changes the order of fields, and removes the username field.
|
120 |
"""
|
121 |
request = kwargs.pop('request', None) |
122 |
if request:
|
123 |
self.ip = request.META.get('REMOTE_ADDR', |
124 |
request.META.get('HTTP_X_REAL_IP', None)) |
125 |
|
126 |
super(LocalUserCreationForm, self).__init__(*args, **kwargs) |
127 |
self.fields.keyOrder = ['email', 'first_name', 'last_name', |
128 |
'password1', 'password2'] |
129 |
|
130 |
if RECAPTCHA_ENABLED:
|
131 |
self.fields.keyOrder.extend(['recaptcha_challenge_field', |
132 |
'recaptcha_response_field', ])
|
133 |
if get_latest_terms():
|
134 |
self.fields.keyOrder.append('has_signed_terms') |
135 |
|
136 |
if 'has_signed_terms' in self.fields: |
137 |
# Overriding field label since we need to apply a link
|
138 |
# to the terms within the label
|
139 |
terms_link_html = '<a href="%s" target="_blank">%s</a>' \
|
140 |
% (reverse('latest_terms'), _("the terms")) |
141 |
self.fields['has_signed_terms'].label = \ |
142 |
mark_safe("I agree with %s" % terms_link_html)
|
143 |
|
144 |
def clean_email(self): |
145 |
email = self.cleaned_data['email'].lower() |
146 |
if not email: |
147 |
raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
|
148 |
if reserved_email(email):
|
149 |
raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
|
150 |
return email
|
151 |
|
152 |
def clean_has_signed_terms(self): |
153 |
has_signed_terms = self.cleaned_data['has_signed_terms'] |
154 |
if not has_signed_terms: |
155 |
raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
|
156 |
return has_signed_terms
|
157 |
|
158 |
def clean_recaptcha_response_field(self): |
159 |
if 'recaptcha_challenge_field' in self.cleaned_data: |
160 |
self.validate_captcha()
|
161 |
return self.cleaned_data['recaptcha_response_field'] |
162 |
|
163 |
def clean_recaptcha_challenge_field(self): |
164 |
if 'recaptcha_response_field' in self.cleaned_data: |
165 |
self.validate_captcha()
|
166 |
return self.cleaned_data['recaptcha_challenge_field'] |
167 |
|
168 |
def validate_captcha(self): |
169 |
rcf = self.cleaned_data['recaptcha_challenge_field'] |
170 |
rrf = self.cleaned_data['recaptcha_response_field'] |
171 |
check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
|
172 |
if not check.is_valid: |
173 |
raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
|
174 |
|
175 |
def post_store_user(self, user, request): |
176 |
"""
|
177 |
Interface method for descendant backends to be able to do stuff within
|
178 |
the transaction enabled by store_user.
|
179 |
"""
|
180 |
user.add_auth_provider('local', auth_backend='astakos') |
181 |
user.set_password(self.cleaned_data['password1']) |
182 |
|
183 |
def save(self, commit=True): |
184 |
"""
|
185 |
Saves the email, first_name and last_name properties, after the normal
|
186 |
save behavior is complete.
|
187 |
"""
|
188 |
user = super(LocalUserCreationForm, self).save(commit=False) |
189 |
user.renew_token() |
190 |
if commit:
|
191 |
user.save() |
192 |
logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
|
193 |
return user
|
194 |
|
195 |
|
196 |
class InvitedLocalUserCreationForm(LocalUserCreationForm): |
197 |
"""
|
198 |
Extends the LocalUserCreationForm: email is readonly.
|
199 |
"""
|
200 |
class Meta: |
201 |
model = AstakosUser |
202 |
fields = ("email", "first_name", "last_name", "has_signed_terms") |
203 |
|
204 |
def __init__(self, *args, **kwargs): |
205 |
"""
|
206 |
Changes the order of fields, and removes the username field.
|
207 |
"""
|
208 |
super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs) |
209 |
|
210 |
#set readonly form fields
|
211 |
ro = ('email', 'username',) |
212 |
for f in ro: |
213 |
self.fields[f].widget.attrs['readonly'] = True |
214 |
|
215 |
def save(self, commit=True): |
216 |
user = super(InvitedLocalUserCreationForm, self).save(commit=False) |
217 |
user.set_invitations_level() |
218 |
user.email_verified = True
|
219 |
if commit:
|
220 |
user.save() |
221 |
return user
|
222 |
|
223 |
|
224 |
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin): |
225 |
id = forms.CharField( |
226 |
widget=forms.HiddenInput(), |
227 |
label='',
|
228 |
required=False
|
229 |
) |
230 |
third_party_identifier = forms.CharField( |
231 |
widget=forms.HiddenInput(), |
232 |
label=''
|
233 |
) |
234 |
class Meta: |
235 |
model = AstakosUser |
236 |
fields = ['id', 'email', 'third_party_identifier', 'first_name', 'last_name'] |
237 |
|
238 |
def __init__(self, *args, **kwargs): |
239 |
"""
|
240 |
Changes the order of fields, and removes the username field.
|
241 |
"""
|
242 |
self.request = kwargs.get('request', None) |
243 |
if self.request: |
244 |
kwargs.pop('request')
|
245 |
|
246 |
latest_terms = get_latest_terms() |
247 |
if latest_terms:
|
248 |
self._meta.fields.append('has_signed_terms') |
249 |
|
250 |
super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs) |
251 |
|
252 |
if latest_terms:
|
253 |
self.fields.keyOrder.append('has_signed_terms') |
254 |
|
255 |
if 'has_signed_terms' in self.fields: |
256 |
# Overriding field label since we need to apply a link
|
257 |
# to the terms within the label
|
258 |
terms_link_html = '<a href="%s" target="_blank">%s</a>' \
|
259 |
% (reverse('latest_terms'), _("the terms")) |
260 |
self.fields['has_signed_terms'].label = \ |
261 |
mark_safe("I agree with %s" % terms_link_html)
|
262 |
|
263 |
def clean_email(self): |
264 |
email = self.cleaned_data['email'].lower() |
265 |
if not email: |
266 |
raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
|
267 |
if reserved_email(email):
|
268 |
raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
|
269 |
return email
|
270 |
|
271 |
def clean_has_signed_terms(self): |
272 |
has_signed_terms = self.cleaned_data['has_signed_terms'] |
273 |
if not has_signed_terms: |
274 |
raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
|
275 |
return has_signed_terms
|
276 |
|
277 |
def post_store_user(self, user, request): |
278 |
pending = PendingThirdPartyUser.objects.get( |
279 |
token=request.POST.get('third_party_token'),
|
280 |
third_party_identifier= \ |
281 |
self.cleaned_data.get('third_party_identifier')) |
282 |
return user.add_pending_auth_provider(pending)
|
283 |
|
284 |
|
285 |
def save(self, commit=True): |
286 |
user = super(ThirdPartyUserCreationForm, self).save(commit=False) |
287 |
user.set_unusable_password() |
288 |
user.renew_token() |
289 |
if commit:
|
290 |
user.save() |
291 |
logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
|
292 |
return user
|
293 |
|
294 |
|
295 |
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm): |
296 |
"""
|
297 |
Extends the ThirdPartyUserCreationForm: email is readonly.
|
298 |
"""
|
299 |
def __init__(self, *args, **kwargs): |
300 |
"""
|
301 |
Changes the order of fields, and removes the username field.
|
302 |
"""
|
303 |
super(
|
304 |
InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
|
305 |
|
306 |
#set readonly form fields
|
307 |
ro = ('email',)
|
308 |
for f in ro: |
309 |
self.fields[f].widget.attrs['readonly'] = True |
310 |
|
311 |
def save(self, commit=True): |
312 |
user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False) |
313 |
user.set_invitation_level() |
314 |
user.email_verified = True
|
315 |
if commit:
|
316 |
user.save() |
317 |
return user
|
318 |
|
319 |
|
320 |
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm): |
321 |
additional_email = forms.CharField( |
322 |
widget=forms.HiddenInput(), label='', required=False) |
323 |
|
324 |
def __init__(self, *args, **kwargs): |
325 |
super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs) |
326 |
# copy email value to additional_mail in case user will change it
|
327 |
name = 'email'
|
328 |
field = self.fields[name]
|
329 |
self.initial['additional_email'] = self.initial.get(name, field.initial) |
330 |
self.initial['email'] = None |
331 |
|
332 |
|
333 |
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, |
334 |
InvitedThirdPartyUserCreationForm): |
335 |
pass
|
336 |
|
337 |
|
338 |
class LoginForm(AuthenticationForm): |
339 |
username = forms.EmailField(label=_("Email"))
|
340 |
recaptcha_challenge_field = forms.CharField(widget=DummyWidget) |
341 |
recaptcha_response_field = forms.CharField( |
342 |
widget=RecaptchaWidget, label='')
|
343 |
|
344 |
def __init__(self, *args, **kwargs): |
345 |
was_limited = kwargs.get('was_limited', False) |
346 |
request = kwargs.get('request', None) |
347 |
if request:
|
348 |
self.ip = request.META.get('REMOTE_ADDR', |
349 |
request.META.get('HTTP_X_REAL_IP', None)) |
350 |
|
351 |
t = ('request', 'was_limited') |
352 |
for elem in t: |
353 |
if elem in kwargs.keys(): |
354 |
kwargs.pop(elem) |
355 |
super(LoginForm, self).__init__(*args, **kwargs) |
356 |
|
357 |
self.fields.keyOrder = ['username', 'password'] |
358 |
if was_limited and RECAPTCHA_ENABLED: |
359 |
self.fields.keyOrder.extend(['recaptcha_challenge_field', |
360 |
'recaptcha_response_field', ])
|
361 |
|
362 |
def clean_username(self): |
363 |
return self.cleaned_data['username'].lower() |
364 |
|
365 |
def clean_recaptcha_response_field(self): |
366 |
if 'recaptcha_challenge_field' in self.cleaned_data: |
367 |
self.validate_captcha()
|
368 |
return self.cleaned_data['recaptcha_response_field'] |
369 |
|
370 |
def clean_recaptcha_challenge_field(self): |
371 |
if 'recaptcha_response_field' in self.cleaned_data: |
372 |
self.validate_captcha()
|
373 |
return self.cleaned_data['recaptcha_challenge_field'] |
374 |
|
375 |
def validate_captcha(self): |
376 |
rcf = self.cleaned_data['recaptcha_challenge_field'] |
377 |
rrf = self.cleaned_data['recaptcha_response_field'] |
378 |
check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
|
379 |
if not check.is_valid: |
380 |
raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
|
381 |
|
382 |
def clean(self): |
383 |
"""
|
384 |
Override default behavior in order to check user's activation later
|
385 |
"""
|
386 |
username = self.cleaned_data.get('username') |
387 |
|
388 |
try:
|
389 |
user = AstakosUser.objects.get(email=username) |
390 |
if not user.has_auth_provider('local'): |
391 |
provider = auth_providers.get_provider('local')
|
392 |
raise forms.ValidationError(
|
393 |
_(provider.get_message('NOT_ACTIVE_FOR_USER_LOGIN')))
|
394 |
except AstakosUser.DoesNotExist:
|
395 |
pass
|
396 |
|
397 |
try:
|
398 |
super(LoginForm, self).clean() |
399 |
except forms.ValidationError, e:
|
400 |
if self.user_cache is None: |
401 |
raise
|
402 |
if not self.user_cache.is_active: |
403 |
raise forms.ValidationError(self.user_cache.get_inactive_message()) |
404 |
if self.request: |
405 |
if not self.request.session.test_cookie_worked(): |
406 |
raise
|
407 |
return self.cleaned_data |
408 |
|
409 |
|
410 |
class ProfileForm(forms.ModelForm): |
411 |
"""
|
412 |
Subclass of ``ModelForm`` for permiting user to edit his/her profile.
|
413 |
Most of the fields are readonly since the user is not allowed to change
|
414 |
them.
|
415 |
|
416 |
The class defines a save method which sets ``is_verified`` to True so as the
|
417 |
user during the next login will not to be redirected to profile page.
|
418 |
"""
|
419 |
renew = forms.BooleanField(label='Renew token', required=False) |
420 |
|
421 |
class Meta: |
422 |
model = AstakosUser |
423 |
fields = ('email', 'first_name', 'last_name', 'auth_token', |
424 |
'auth_token_expires')
|
425 |
|
426 |
def __init__(self, *args, **kwargs): |
427 |
self.session_key = kwargs.pop('session_key', None) |
428 |
super(ProfileForm, self).__init__(*args, **kwargs) |
429 |
instance = getattr(self, 'instance', None) |
430 |
ro_fields = ('email', 'auth_token', 'auth_token_expires') |
431 |
if instance and instance.id: |
432 |
for field in ro_fields: |
433 |
self.fields[field].widget.attrs['readonly'] = True |
434 |
|
435 |
def save(self, commit=True): |
436 |
user = super(ProfileForm, self).save(commit=False) |
437 |
user.is_verified = True
|
438 |
if self.cleaned_data.get('renew'): |
439 |
user.renew_token( |
440 |
flush_sessions=True,
|
441 |
current_key=self.session_key
|
442 |
) |
443 |
if commit:
|
444 |
user.save() |
445 |
return user
|
446 |
|
447 |
|
448 |
class FeedbackForm(forms.Form): |
449 |
"""
|
450 |
Form for writing feedback.
|
451 |
"""
|
452 |
feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
|
453 |
feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
|
454 |
required=False)
|
455 |
|
456 |
|
457 |
class SendInvitationForm(forms.Form): |
458 |
"""
|
459 |
Form for sending an invitations
|
460 |
"""
|
461 |
|
462 |
email = forms.EmailField(required=True, label='Email address') |
463 |
first_name = forms.EmailField(label='First name')
|
464 |
last_name = forms.EmailField(label='Last name')
|
465 |
|
466 |
|
467 |
class ExtendedPasswordResetForm(PasswordResetForm): |
468 |
"""
|
469 |
Extends PasswordResetForm by overriding save method:
|
470 |
passes a custom from_email in send_mail.
|
471 |
|
472 |
Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
|
473 |
accepts a from_email argument.
|
474 |
"""
|
475 |
def clean_email(self): |
476 |
email = super(ExtendedPasswordResetForm, self).clean_email() |
477 |
try:
|
478 |
user = AstakosUser.objects.get(email__iexact=email) |
479 |
if not user.has_usable_password(): |
480 |
raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
|
481 |
|
482 |
if not user.can_change_password(): |
483 |
raise forms.ValidationError(_('Password change for this account' |
484 |
' is not supported.'))
|
485 |
|
486 |
except AstakosUser.DoesNotExist, e:
|
487 |
raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
|
488 |
return email
|
489 |
|
490 |
def save( |
491 |
self, domain_override=None, email_template_name='registration/password_reset_email.html', |
492 |
use_https=False, token_generator=default_token_generator, request=None): |
493 |
"""
|
494 |
Generates a one-use only link for resetting password and sends to the user.
|
495 |
"""
|
496 |
for user in self.users_cache: |
497 |
url = user.astakosuser.get_password_reset_url(token_generator) |
498 |
url = urljoin(BASEURL, url) |
499 |
t = loader.get_template(email_template_name) |
500 |
c = { |
501 |
'email': user.email,
|
502 |
'url': url,
|
503 |
'site_name': SITENAME,
|
504 |
'user': user,
|
505 |
'baseurl': BASEURL,
|
506 |
'support': DEFAULT_CONTACT_EMAIL
|
507 |
} |
508 |
from_email = settings.SERVER_EMAIL |
509 |
send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT), |
510 |
t.render(Context(c)), from_email, [user.email]) |
511 |
|
512 |
|
513 |
class EmailChangeForm(forms.ModelForm): |
514 |
class Meta: |
515 |
model = EmailChange |
516 |
fields = ('new_email_address',)
|
517 |
|
518 |
def clean_new_email_address(self): |
519 |
addr = self.cleaned_data['new_email_address'] |
520 |
if AstakosUser.objects.filter(email__iexact=addr):
|
521 |
raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
|
522 |
return addr
|
523 |
|
524 |
def save(self, email_template_name, request, commit=True): |
525 |
ec = super(EmailChangeForm, self).save(commit=False) |
526 |
ec.user = request.user |
527 |
activation_key = hashlib.sha1( |
528 |
str(random()) + smart_str(ec.new_email_address))
|
529 |
ec.activation_key = activation_key.hexdigest() |
530 |
if commit:
|
531 |
ec.save() |
532 |
send_change_email(ec, request, email_template_name=email_template_name) |
533 |
|
534 |
|
535 |
class SignApprovalTermsForm(forms.ModelForm): |
536 |
class Meta: |
537 |
model = AstakosUser |
538 |
fields = ("has_signed_terms",)
|
539 |
|
540 |
def __init__(self, *args, **kwargs): |
541 |
super(SignApprovalTermsForm, self).__init__(*args, **kwargs) |
542 |
|
543 |
def clean_has_signed_terms(self): |
544 |
has_signed_terms = self.cleaned_data['has_signed_terms'] |
545 |
if not has_signed_terms: |
546 |
raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
|
547 |
return has_signed_terms
|
548 |
|
549 |
|
550 |
class InvitationForm(forms.ModelForm): |
551 |
username = forms.EmailField(label=_("Email"))
|
552 |
|
553 |
def __init__(self, *args, **kwargs): |
554 |
super(InvitationForm, self).__init__(*args, **kwargs) |
555 |
|
556 |
class Meta: |
557 |
model = Invitation |
558 |
fields = ('username', 'realname') |
559 |
|
560 |
def clean_username(self): |
561 |
username = self.cleaned_data['username'] |
562 |
try:
|
563 |
Invitation.objects.get(username=username) |
564 |
raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
|
565 |
except Invitation.DoesNotExist:
|
566 |
pass
|
567 |
return username
|
568 |
|
569 |
|
570 |
class ExtendedPasswordChangeForm(PasswordChangeForm): |
571 |
"""
|
572 |
Extends PasswordChangeForm by enabling user
|
573 |
to optionally renew also the token.
|
574 |
"""
|
575 |
if not NEWPASSWD_INVALIDATE_TOKEN: |
576 |
renew = forms.BooleanField(label='Renew token', required=False, |
577 |
initial=True,
|
578 |
help_text='Unsetting this may result in security risk.')
|
579 |
|
580 |
def __init__(self, user, *args, **kwargs): |
581 |
self.session_key = kwargs.pop('session_key', None) |
582 |
super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs) |
583 |
|
584 |
def save(self, commit=True): |
585 |
try:
|
586 |
if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'): |
587 |
self.user.renew_token()
|
588 |
self.user.flush_sessions(current_key=self.session_key) |
589 |
except AttributeError: |
590 |
# if user model does has not such methods
|
591 |
pass
|
592 |
return super(ExtendedPasswordChangeForm, self).save(commit=commit) |
593 |
|
594 |
|
595 |
# class AstakosGroupCreationForm(forms.ModelForm):
|
596 |
# kind = forms.ModelChoiceField(
|
597 |
# queryset=GroupKind.objects.all(),
|
598 |
# label="",
|
599 |
# widget=forms.HiddenInput()
|
600 |
# )
|
601 |
# name = forms.CharField(
|
602 |
# validators=[validators.RegexValidator(
|
603 |
# DOMAIN_VALUE_REGEX,
|
604 |
# _(astakos_messages.DOMAIN_VALUE_ERR), 'invalid'
|
605 |
# )],
|
606 |
# widget=forms.TextInput(attrs={'placeholder': 'myproject.mylab.ntua.gr'}),
|
607 |
# help_text=" The Project's name should be in a domain format. The domain shouldn't neccessarily exist in the real world but is helpful to imply a structure. e.g.: myproject.mylab.ntua.gr or myservice.myteam.myorganization "
|
608 |
# )
|
609 |
# homepage = forms.URLField(
|
610 |
# label= 'Homepage Url',
|
611 |
# widget=forms.TextInput(attrs={'placeholder': 'http://myproject.com'}),
|
612 |
# help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",
|
613 |
# required=False
|
614 |
# )
|
615 |
# desc = forms.CharField(
|
616 |
# label= 'Description',
|
617 |
# widget=forms.Textarea,
|
618 |
# help_text= "Please provide a short but descriptive abstract of your Project, so that anyone searching can quickly understand what this Project is about. "
|
619 |
# )
|
620 |
# issue_date = forms.DateTimeField(
|
621 |
# label= 'Start date',
|
622 |
# help_text= "Here you specify the date you want your Project to start granting its resources. Its members will get the resources coming from this Project on this exact date."
|
623 |
# )
|
624 |
# expiration_date = forms.DateTimeField(
|
625 |
# label= 'End date',
|
626 |
# help_text= "Here you specify the date you want your Project to cease. This means that after this date all members will no longer be able to allocate resources from this Project. "
|
627 |
# )
|
628 |
# moderation_enabled = forms.BooleanField(
|
629 |
# label= 'Moderated',
|
630 |
# help_text="Select this to approve each member manually, before they become a part of your Project (default). Be sure you know what you are doing, if you uncheck this option. ",
|
631 |
# required=False,
|
632 |
# initial=True
|
633 |
# )
|
634 |
# max_participants = forms.IntegerField(
|
635 |
# label='Total number of members',
|
636 |
# required=True, min_value=1,
|
637 |
# help_text="Here you specify the number of members this Project is going to have. This means that this number of people will be granted the resources you will specify in the next step. This can be '1' if you are the only one wanting to get resources. "
|
638 |
# )
|
639 |
#
|
640 |
# class Meta:
|
641 |
# model = AstakosGroup
|
642 |
#
|
643 |
# def __init__(self, *args, **kwargs):
|
644 |
# #update QueryDict
|
645 |
# args = list(args)
|
646 |
# qd = args.pop(0).copy()
|
647 |
# members_unlimited = qd.pop('members_unlimited', False)
|
648 |
# members_uplimit = qd.pop('members_uplimit', None)
|
649 |
#
|
650 |
# #substitue QueryDict
|
651 |
# args.insert(0, qd)
|
652 |
#
|
653 |
# super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
|
654 |
#
|
655 |
# self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
|
656 |
# 'issue_date', 'expiration_date',
|
657 |
# 'moderation_enabled', 'max_participants']
|
658 |
# def add_fields((k, v)):
|
659 |
# k = k.partition('_proxy')[0]
|
660 |
# self.fields[k] = forms.IntegerField(
|
661 |
# required=False,
|
662 |
# widget=forms.HiddenInput(),
|
663 |
# min_value=1
|
664 |
# )
|
665 |
# map(add_fields,
|
666 |
# ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
|
667 |
# )
|
668 |
#
|
669 |
# def add_fields((k, v)):
|
670 |
# self.fields[k] = forms.BooleanField(
|
671 |
# required=False,
|
672 |
# #widget=forms.HiddenInput()
|
673 |
# )
|
674 |
# map(add_fields,
|
675 |
# ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
|
676 |
# )
|
677 |
#
|
678 |
# def policies(self):
|
679 |
# self.clean()
|
680 |
# policies = []
|
681 |
# append = policies.append
|
682 |
# for name, uplimit in self.cleaned_data.iteritems():
|
683 |
#
|
684 |
# subs = name.split('_uplimit')
|
685 |
# if len(subs) == 2:
|
686 |
# prefix, suffix = subs
|
687 |
# s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
|
688 |
# resource = Resource.objects.get(service__name=s, name=r)
|
689 |
#
|
690 |
# # keep only resource limits for selected resource groups
|
691 |
# if self.cleaned_data.get(
|
692 |
# 'is_selected_%s' % resource.group, False
|
693 |
# ):
|
694 |
# append(dict(service=s, resource=r, uplimit=uplimit))
|
695 |
# return policies
|
696 |
#
|
697 |
# class AstakosGroupCreationSummaryForm(forms.ModelForm):
|
698 |
# kind = forms.ModelChoiceField(
|
699 |
# queryset=GroupKind.objects.all(),
|
700 |
# label="",
|
701 |
# widget=forms.HiddenInput()
|
702 |
# )
|
703 |
# name = forms.CharField(
|
704 |
# widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}),
|
705 |
# help_text="Name should be in the form of dns"
|
706 |
# )
|
707 |
# moderation_enabled = forms.BooleanField(
|
708 |
# help_text="Check if you want to approve members participation manually",
|
709 |
# required=False,
|
710 |
# initial=True
|
711 |
# )
|
712 |
# max_participants = forms.IntegerField(
|
713 |
# required=False, min_value=1
|
714 |
# )
|
715 |
#
|
716 |
# class Meta:
|
717 |
# model = AstakosGroup
|
718 |
#
|
719 |
# def __init__(self, *args, **kwargs):
|
720 |
# #update QueryDict
|
721 |
# args = list(args)
|
722 |
# qd = args.pop(0).copy()
|
723 |
# members_unlimited = qd.pop('members_unlimited', False)
|
724 |
# members_uplimit = qd.pop('members_uplimit', None)
|
725 |
#
|
726 |
# #substitue QueryDict
|
727 |
# args.insert(0, qd)
|
728 |
#
|
729 |
# super(AstakosGroupCreationSummaryForm, self).__init__(*args, **kwargs)
|
730 |
# self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
|
731 |
# 'issue_date', 'expiration_date',
|
732 |
# 'moderation_enabled', 'max_participants']
|
733 |
# def add_fields((k, v)):
|
734 |
# self.fields[k] = forms.IntegerField(
|
735 |
# required=False,
|
736 |
# widget=forms.TextInput(),
|
737 |
# min_value=1
|
738 |
# )
|
739 |
# map(add_fields,
|
740 |
# ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
|
741 |
# )
|
742 |
#
|
743 |
# def add_fields((k, v)):
|
744 |
# self.fields[k] = forms.BooleanField(
|
745 |
# required=False,
|
746 |
# widget=forms.HiddenInput()
|
747 |
# )
|
748 |
# map(add_fields,
|
749 |
# ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
|
750 |
# )
|
751 |
# for f in self.fields.values():
|
752 |
# f.widget = forms.HiddenInput()
|
753 |
#
|
754 |
# def clean(self):
|
755 |
# super(AstakosGroupCreationSummaryForm, self).clean()
|
756 |
# self.cleaned_data['policies'] = []
|
757 |
# append = self.cleaned_data['policies'].append
|
758 |
# #tbd = [f for f in self.fields if (f.startswith('is_selected_') and (not f.endswith('_proxy')))]
|
759 |
# tbd = [f for f in self.fields if f.startswith('is_selected_')]
|
760 |
# for name, uplimit in self.cleaned_data.iteritems():
|
761 |
# subs = name.split('_uplimit')
|
762 |
# if len(subs) == 2:
|
763 |
# tbd.append(name)
|
764 |
# prefix, suffix = subs
|
765 |
# s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
|
766 |
# resource = Resource.objects.get(service__name=s, name=r)
|
767 |
#
|
768 |
# # keep only resource limits for selected resource groups
|
769 |
# if self.cleaned_data.get(
|
770 |
# 'is_selected_%s' % resource.group, False
|
771 |
# ):
|
772 |
# append(dict(service=s, resource=r, uplimit=uplimit))
|
773 |
# for name in tbd:
|
774 |
# self.cleaned_data.pop(name, None)
|
775 |
# return self.cleaned_data
|
776 |
#
|
777 |
# class AstakosGroupUpdateForm(forms.ModelForm):
|
778 |
# class Meta:
|
779 |
# model = AstakosGroup
|
780 |
# fields = ( 'desc','homepage', 'moderation_enabled')
|
781 |
#
|
782 |
#
|
783 |
# class AddGroupMembersForm(forms.Form):
|
784 |
# q = forms.CharField(
|
785 |
# max_length=800, widget=forms.Textarea, label=_('Add members'),
|
786 |
# help_text=_(astakos_messages.ADD_GROUP_MEMBERS_Q_HELP),
|
787 |
# required=True)
|
788 |
#
|
789 |
# def clean(self):
|
790 |
# q = self.cleaned_data.get('q') or ''
|
791 |
# users = q.split(',')
|
792 |
# users = list(u.strip() for u in users if u)
|
793 |
# db_entries = AstakosUser.objects.filter(email__in=users)
|
794 |
# unknown = list(set(users) - set(u.email for u in db_entries))
|
795 |
# if unknown:
|
796 |
# raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
|
797 |
# self.valid_users = db_entries
|
798 |
# return self.cleaned_data
|
799 |
#
|
800 |
# def get_valid_users(self):
|
801 |
# """Should be called after form cleaning"""
|
802 |
# try:
|
803 |
# return self.valid_users
|
804 |
# except:
|
805 |
# return ()
|
806 |
#
|
807 |
#
|
808 |
# class AstakosGroupSearchForm(forms.Form):
|
809 |
# q = forms.CharField(max_length=200, label='Search project')
|
810 |
#
|
811 |
#
|
812 |
# class TimelineForm(forms.Form):
|
813 |
# entity = forms.ModelChoiceField(
|
814 |
# queryset=AstakosUser.objects.filter(is_active=True)
|
815 |
# )
|
816 |
# resource = forms.ModelChoiceField(
|
817 |
# queryset=Resource.objects.all()
|
818 |
# )
|
819 |
# start_date = forms.DateTimeField()
|
820 |
# end_date = forms.DateTimeField()
|
821 |
# details = forms.BooleanField(required=False, label="Detailed Listing")
|
822 |
# operation = forms.ChoiceField(
|
823 |
# label='Charge Method',
|
824 |
# choices=(('', '-------------'),
|
825 |
# ('charge_usage', 'Charge Usage'),
|
826 |
# ('charge_traffic', 'Charge Traffic'), )
|
827 |
# )
|
828 |
#
|
829 |
# def clean(self):
|
830 |
# super(TimelineForm, self).clean()
|
831 |
# d = self.cleaned_data
|
832 |
# if 'resource' in d:
|
833 |
# d['resource'] = str(d['resource'])
|
834 |
# if 'start_date' in d:
|
835 |
# d['start_date'] = d['start_date'].strftime(
|
836 |
# "%Y-%m-%dT%H:%M:%S.%f")[:24]
|
837 |
# if 'end_date' in d:
|
838 |
# d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
|
839 |
# if 'entity' in d:
|
840 |
# d['entity'] = d['entity'].email
|
841 |
# return d
|
842 |
#
|
843 |
#
|
844 |
# class AstakosGroupSortForm(forms.Form):
|
845 |
# sorting = forms.ChoiceField(
|
846 |
# label='Sort by',
|
847 |
# choices=(
|
848 |
# ('groupname', 'Name'),
|
849 |
# ('issue_date', 'Issue Date'),
|
850 |
# ('expiration_date', 'Expiration Date'),
|
851 |
# ('approved_members_num', 'Participants'),
|
852 |
# ('moderation_enabled', 'Moderation'),
|
853 |
# ('membership_status', 'Enrollment Status')
|
854 |
# ),
|
855 |
# required=True
|
856 |
# )
|
857 |
#
|
858 |
# class MembersSortForm(forms.Form):
|
859 |
# sorting = forms.ChoiceField(
|
860 |
# label='Sort by',
|
861 |
# choices=(('person__email', 'User Id'),
|
862 |
# ('person__first_name', 'Name'),
|
863 |
# ('date_joined', 'Status')
|
864 |
# ),
|
865 |
# required=True
|
866 |
# )
|
867 |
#
|
868 |
# class PickResourceForm(forms.Form):
|
869 |
# resource = forms.ModelChoiceField(
|
870 |
# queryset=Resource.objects.select_related().all()
|
871 |
# )
|
872 |
# resource.widget.attrs["onchange"] = "this.form.submit()"
|
873 |
|
874 |
|
875 |
class ExtendedSetPasswordForm(SetPasswordForm): |
876 |
"""
|
877 |
Extends SetPasswordForm by enabling user
|
878 |
to optionally renew also the token.
|
879 |
"""
|
880 |
if not NEWPASSWD_INVALIDATE_TOKEN: |
881 |
renew = forms.BooleanField( |
882 |
label='Renew token',
|
883 |
required=False,
|
884 |
initial=True,
|
885 |
help_text='Unsetting this may result in security risk.')
|
886 |
|
887 |
def __init__(self, user, *args, **kwargs): |
888 |
super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs) |
889 |
|
890 |
@transaction.commit_on_success()
|
891 |
def save(self, commit=True): |
892 |
try:
|
893 |
self.user = AstakosUser.objects.get(id=self.user.id) |
894 |
if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'): |
895 |
self.user.renew_token()
|
896 |
#self.user.flush_sessions()
|
897 |
if not self.user.has_auth_provider('local'): |
898 |
self.user.add_auth_provider('local', auth_backend='astakos') |
899 |
|
900 |
except BaseException, e: |
901 |
logger.exception(e) |
902 |
return super(ExtendedSetPasswordForm, self).save(commit=commit) |
903 |
|
904 |
|
905 |
class ProjectApplicationForm(forms.ModelForm): |
906 |
name = forms.CharField( |
907 |
validators=[validators.RegexValidator( |
908 |
DOMAIN_VALUE_REGEX, |
909 |
_(astakos_messages.DOMAIN_VALUE_ERR), |
910 |
'invalid'
|
911 |
)], |
912 |
widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}), |
913 |
help_text="Name should be in the form of dns"
|
914 |
) |
915 |
comments = forms.CharField(widget=forms.Textarea, required=False)
|
916 |
|
917 |
class Meta: |
918 |
model = ProjectApplication |
919 |
exclude = ( |
920 |
'resource_grants', 'id', 'applicant', 'owner', |
921 |
'precursor_application', 'state', 'issue_date') |
922 |
|
923 |
def clean(self): |
924 |
userid = self.data.get('user', None) |
925 |
self.user = None |
926 |
if userid:
|
927 |
try:
|
928 |
self.user = AstakosUser.objects.get(id=userid)
|
929 |
except AstakosUser.DoesNotExist:
|
930 |
pass
|
931 |
if not self.user: |
932 |
raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
|
933 |
super(ProjectApplicationForm, self).clean() |
934 |
return self.cleaned_data |
935 |
|
936 |
@property
|
937 |
def resource_policies(self): |
938 |
policies = [] |
939 |
append = policies.append |
940 |
for name, value in self.data.iteritems(): |
941 |
if not value: |
942 |
continue
|
943 |
uplimit = value |
944 |
if name.endswith('_uplimit'): |
945 |
subs = name.split('_uplimit')
|
946 |
prefix, suffix = subs |
947 |
s, sep, r = prefix.partition(RESOURCE_SEPARATOR) |
948 |
resource = Resource.objects.get(service__name=s, name=r) |
949 |
|
950 |
# keep only resource limits for selected resource groups
|
951 |
# if self.data.get(
|
952 |
# 'is_selected_%s' % resource.group, False
|
953 |
# ):
|
954 |
if uplimit:
|
955 |
append(dict(service=s, resource=r, uplimit=uplimit))
|
956 |
return policies
|
957 |
|
958 |
def save(self, commit=True): |
959 |
application = super(ProjectApplicationForm, self).save(commit=False) |
960 |
applicant = self.user
|
961 |
comments = self.cleaned_data.pop('comments', None) |
962 |
try:
|
963 |
precursor_application = self.instance.precursor_application
|
964 |
except:
|
965 |
precursor_application = None
|
966 |
return submit_application(
|
967 |
application, |
968 |
self.resource_policies,
|
969 |
applicant, |
970 |
comments, |
971 |
precursor_application |
972 |
) |
973 |
|
974 |
class ProjectSortForm(forms.Form): |
975 |
sorting = forms.ChoiceField( |
976 |
label='Sort by',
|
977 |
choices=(('name', 'Sort by Name'), |
978 |
('issue_date', 'Sort by Issue date'), |
979 |
('start_date', 'Sort by Start Date'), |
980 |
('end_date', 'Sort by End Date'), |
981 |
# ('approved_members_num', 'Sort by Participants'),
|
982 |
('state', 'Sort by Status'), |
983 |
('member_join_policy__description', 'Sort by Member Join Policy'), |
984 |
('member_leave_policy__description', 'Sort by Member Leave Policy') |
985 |
), |
986 |
required=True
|
987 |
) |
988 |
|
989 |
class AddProjectMembersForm(forms.Form): |
990 |
q = forms.CharField( |
991 |
max_length=800, widget=forms.Textarea, label=_('Add members'), |
992 |
help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), |
993 |
required=True)
|
994 |
|
995 |
def clean(self): |
996 |
q = self.cleaned_data.get('q') or '' |
997 |
users = q.split(',')
|
998 |
users = list(u.strip() for u in users if u) |
999 |
db_entries = AstakosUser.objects.filter(email__in=users) |
1000 |
unknown = list(set(users) - set(u.email for u in db_entries)) |
1001 |
if unknown:
|
1002 |
raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown)) |
1003 |
self.valid_users = db_entries
|
1004 |
return self.cleaned_data |
1005 |
|
1006 |
def get_valid_users(self): |
1007 |
"""Should be called after form cleaning"""
|
1008 |
try:
|
1009 |
return self.valid_users |
1010 |
except:
|
1011 |
return ()
|
1012 |
|
1013 |
class ProjectMembersSortForm(forms.Form): |
1014 |
sorting = forms.ChoiceField( |
1015 |
label='Sort by',
|
1016 |
choices=(('person__email', 'User Id'), |
1017 |
('person__first_name', 'Name'), |
1018 |
('acceptance_date', 'Acceptance date') |
1019 |
), |
1020 |
required=True
|
1021 |
) |
1022 |
|
1023 |
class ProjectSearchForm(forms.Form): |
1024 |
q = forms.CharField(max_length=200, label='Search project') |