1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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 datetime import datetime
36 from django import forms
37 from django.utils.translation import ugettext as _
38 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \
39 PasswordResetForm, PasswordChangeForm
40 from django.core.mail import send_mail
41 from django.contrib.auth.tokens import default_token_generator
42 from django.template import Context, loader
43 from django.utils.http import int_to_base36
44 from django.core.urlresolvers import reverse
45 from django.utils.functional import lazy
46 from django.utils.safestring import mark_safe
47 from django.contrib import messages
48 from django.utils.encoding import smart_str
49 from django.forms.extras.widgets import SelectDateWidget
50 from django.db.models import Q
51 from django.db.models.query import EmptyQuerySet
53 from astakos.im.models import *
54 from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, \
55 BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, \
56 RECAPTCHA_ENABLED, LOGGING_LEVEL
57 from astakos.im.widgets import DummyWidget, RecaptchaWidget
58 from astakos.im.functions import send_change_email
60 # since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
61 from astakos.im.util import reverse_lazy, reserved_email, get_query
65 import recaptcha.client.captcha as captcha
66 from random import random
68 logger = logging.getLogger(__name__)
70 class LocalUserCreationForm(UserCreationForm):
72 Extends the built in UserCreationForm in several ways:
74 * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
75 * The username field isn't visible and it is assigned a generated id.
76 * User created is not active.
78 recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
79 recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
83 fields = ("email", "first_name", "last_name", "has_signed_terms", "has_signed_terms")
85 def __init__(self, *args, **kwargs):
87 Changes the order of fields, and removes the username field.
89 request = kwargs.get('request', None)
92 self.ip = request.META.get('REMOTE_ADDR',
93 request.META.get('HTTP_X_REAL_IP', None))
95 super(LocalUserCreationForm, self).__init__(*args, **kwargs)
96 self.fields.keyOrder = ['email', 'first_name', 'last_name',
97 'password1', 'password2']
100 self.fields.keyOrder.extend(['recaptcha_challenge_field',
101 'recaptcha_response_field',])
102 if get_latest_terms():
103 self.fields.keyOrder.append('has_signed_terms')
105 if 'has_signed_terms' in self.fields:
106 # Overriding field label since we need to apply a link
107 # to the terms within the label
108 terms_link_html = '<a href="%s" target="_blank">%s</a>' \
109 % (reverse('latest_terms'), _("the terms"))
110 self.fields['has_signed_terms'].label = \
111 mark_safe("I agree with %s" % terms_link_html)
113 def clean_email(self):
114 email = self.cleaned_data['email']
116 raise forms.ValidationError(_("This field is required"))
117 if reserved_email(email):
118 raise forms.ValidationError(_("This email is already used"))
121 def clean_has_signed_terms(self):
122 has_signed_terms = self.cleaned_data['has_signed_terms']
123 if not has_signed_terms:
124 raise forms.ValidationError(_('You have to agree with the terms'))
125 return has_signed_terms
127 def clean_recaptcha_response_field(self):
128 if 'recaptcha_challenge_field' in self.cleaned_data:
129 self.validate_captcha()
130 return self.cleaned_data['recaptcha_response_field']
132 def clean_recaptcha_challenge_field(self):
133 if 'recaptcha_response_field' in self.cleaned_data:
134 self.validate_captcha()
135 return self.cleaned_data['recaptcha_challenge_field']
137 def validate_captcha(self):
138 rcf = self.cleaned_data['recaptcha_challenge_field']
139 rrf = self.cleaned_data['recaptcha_response_field']
140 check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
141 if not check.is_valid:
142 raise forms.ValidationError(_('You have not entered the correct words'))
144 def save(self, commit=True):
146 Saves the email, first_name and last_name properties, after the normal
147 save behavior is complete.
149 user = super(LocalUserCreationForm, self).save(commit=False)
153 logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, [])
156 class InvitedLocalUserCreationForm(LocalUserCreationForm):
158 Extends the LocalUserCreationForm: email is readonly.
162 fields = ("email", "first_name", "last_name", "has_signed_terms")
164 def __init__(self, *args, **kwargs):
166 Changes the order of fields, and removes the username field.
168 super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
170 #set readonly form fields
171 ro = ('email', 'username',)
173 self.fields[f].widget.attrs['readonly'] = True
176 def save(self, commit=True):
177 user = super(InvitedLocalUserCreationForm, self).save(commit=False)
178 level = user.invitation.inviter.level + 1
180 user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
181 user.email_verified = True
186 class ThirdPartyUserCreationForm(forms.ModelForm):
189 fields = ("email", "first_name", "last_name", "third_party_identifier", "has_signed_terms")
191 def __init__(self, *args, **kwargs):
193 Changes the order of fields, and removes the username field.
195 self.request = kwargs.get('request', None)
197 kwargs.pop('request')
198 super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
199 self.fields.keyOrder = ['email', 'first_name', 'last_name', 'third_party_identifier']
200 if get_latest_terms():
201 self.fields.keyOrder.append('has_signed_terms')
202 #set readonly form fields
203 ro = ["third_party_identifier"]
205 self.fields[f].widget.attrs['readonly'] = True
207 if 'has_signed_terms' in self.fields:
208 # Overriding field label since we need to apply a link
209 # to the terms within the label
210 terms_link_html = '<a href="%s" target="_blank">%s</a>' \
211 % (reverse('latest_terms'), _("the terms"))
212 self.fields['has_signed_terms'].label = \
213 mark_safe("I agree with %s" % terms_link_html)
215 def clean_email(self):
216 email = self.cleaned_data['email']
218 raise forms.ValidationError(_("This field is required"))
221 def clean_has_signed_terms(self):
222 has_signed_terms = self.cleaned_data['has_signed_terms']
223 if not has_signed_terms:
224 raise forms.ValidationError(_('You have to agree with the terms'))
225 return has_signed_terms
227 def save(self, commit=True):
228 user = super(ThirdPartyUserCreationForm, self).save(commit=False)
229 user.set_unusable_password()
231 user.provider = get_query(self.request).get('provider')
234 logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, [])
237 class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
239 Extends the ThirdPartyUserCreationForm: email is readonly.
241 def __init__(self, *args, **kwargs):
243 Changes the order of fields, and removes the username field.
245 super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
247 #set readonly form fields
250 self.fields[f].widget.attrs['readonly'] = True
252 def save(self, commit=True):
253 user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
254 level = user.invitation.inviter.level + 1
256 user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
257 user.email_verified = True
262 class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
263 additional_email = forms.CharField(widget=forms.HiddenInput(), label='', required = False)
265 def __init__(self, *args, **kwargs):
266 super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
267 self.fields.keyOrder.append('additional_email')
268 # copy email value to additional_mail in case user will change it
270 field = self.fields[name]
271 self.initial['additional_email'] = self.initial.get(name, field.initial)
273 def clean_email(self):
274 email = self.cleaned_data['email']
275 for user in AstakosUser.objects.filter(email = email):
276 if user.provider == 'shibboleth':
277 raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
278 elif not user.is_active:
279 raise forms.ValidationError(_("This email is already associated with an inactive account. \
280 You need to wait to be activated before being able to switch to a shibboleth account."))
281 super(ShibbolethUserCreationForm, self).clean_email()
284 class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
287 class LoginForm(AuthenticationForm):
288 username = forms.EmailField(label=_("Email"))
289 recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
290 recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
292 def __init__(self, *args, **kwargs):
293 was_limited = kwargs.get('was_limited', False)
294 request = kwargs.get('request', None)
296 self.ip = request.META.get('REMOTE_ADDR',
297 request.META.get('HTTP_X_REAL_IP', None))
299 t = ('request', 'was_limited')
301 if elem in kwargs.keys():
303 super(LoginForm, self).__init__(*args, **kwargs)
305 self.fields.keyOrder = ['username', 'password']
306 if was_limited and RECAPTCHA_ENABLED:
307 self.fields.keyOrder.extend(['recaptcha_challenge_field',
308 'recaptcha_response_field',])
310 def clean_recaptcha_response_field(self):
311 if 'recaptcha_challenge_field' in self.cleaned_data:
312 self.validate_captcha()
313 return self.cleaned_data['recaptcha_response_field']
315 def clean_recaptcha_challenge_field(self):
316 if 'recaptcha_response_field' in self.cleaned_data:
317 self.validate_captcha()
318 return self.cleaned_data['recaptcha_challenge_field']
320 def validate_captcha(self):
321 rcf = self.cleaned_data['recaptcha_challenge_field']
322 rrf = self.cleaned_data['recaptcha_response_field']
323 check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
324 if not check.is_valid:
325 raise forms.ValidationError(_('You have not entered the correct words'))
328 super(LoginForm, self).clean()
329 if self.user_cache and self.user_cache.provider not in ('local', ''):
330 raise forms.ValidationError(_('Local login is not the current authentication method for this account.'))
331 return self.cleaned_data
333 class ProfileForm(forms.ModelForm):
335 Subclass of ``ModelForm`` for permiting user to edit his/her profile.
336 Most of the fields are readonly since the user is not allowed to change them.
338 The class defines a save method which sets ``is_verified`` to True so as the user
339 during the next login will not to be redirected to profile page.
341 renew = forms.BooleanField(label='Renew token', required=False)
345 fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires')
347 def __init__(self, *args, **kwargs):
348 super(ProfileForm, self).__init__(*args, **kwargs)
349 instance = getattr(self, 'instance', None)
350 ro_fields = ('email', 'auth_token', 'auth_token_expires')
351 if instance and instance.id:
352 for field in ro_fields:
353 self.fields[field].widget.attrs['readonly'] = True
355 def save(self, commit=True):
356 user = super(ProfileForm, self).save(commit=False)
357 user.is_verified = True
358 if self.cleaned_data.get('renew'):
364 class FeedbackForm(forms.Form):
366 Form for writing feedback.
368 feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
369 feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
372 class SendInvitationForm(forms.Form):
374 Form for sending an invitations
377 email = forms.EmailField(required = True, label = 'Email address')
378 first_name = forms.EmailField(label = 'First name')
379 last_name = forms.EmailField(label = 'Last name')
381 class ExtendedPasswordResetForm(PasswordResetForm):
383 Extends PasswordResetForm by overriding save method:
384 passes a custom from_email in send_mail.
386 Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
387 accepts a from_email argument.
389 def clean_email(self):
390 email = super(ExtendedPasswordResetForm, self).clean_email()
392 user = AstakosUser.objects.get(email=email, is_active=True)
393 if not user.has_usable_password():
394 raise forms.ValidationError(_("This account has not a usable password."))
395 except AstakosUser.DoesNotExist, e:
396 raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
399 def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
400 use_https=False, token_generator=default_token_generator, request=None):
402 Generates a one-use only link for resetting password and sends to the user.
404 for user in self.users_cache:
405 url = reverse('django.contrib.auth.views.password_reset_confirm',
406 kwargs={'uidb36':int_to_base36(user.id),
407 'token':token_generator.make_token(user)})
408 url = urljoin(BASEURL, url)
409 t = loader.get_template(email_template_name)
413 'site_name': SITENAME,
416 'support': DEFAULT_CONTACT_EMAIL
418 from_email = DEFAULT_FROM_EMAIL
419 send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
420 t.render(Context(c)), from_email, [user.email])
422 class EmailChangeForm(forms.ModelForm):
425 fields = ('new_email_address',)
427 def clean_new_email_address(self):
428 addr = self.cleaned_data['new_email_address']
429 if AstakosUser.objects.filter(email__iexact=addr):
430 raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
433 def save(self, email_template_name, request, commit=True):
434 ec = super(EmailChangeForm, self).save(commit=False)
435 ec.user = request.user
436 activation_key = hashlib.sha1(str(random()) + smart_str(ec.new_email_address))
437 ec.activation_key=activation_key.hexdigest()
440 send_change_email(ec, request, email_template_name=email_template_name)
442 class SignApprovalTermsForm(forms.ModelForm):
445 fields = ("has_signed_terms",)
447 def __init__(self, *args, **kwargs):
448 super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
450 def clean_has_signed_terms(self):
451 has_signed_terms = self.cleaned_data['has_signed_terms']
452 if not has_signed_terms:
453 raise forms.ValidationError(_('You have to agree with the terms'))
454 return has_signed_terms
456 class InvitationForm(forms.ModelForm):
457 username = forms.EmailField(label=_("Email"))
459 def __init__(self, *args, **kwargs):
460 super(InvitationForm, self).__init__(*args, **kwargs)
464 fields = ('username', 'realname')
466 def clean_username(self):
467 username = self.cleaned_data['username']
469 Invitation.objects.get(username = username)
470 raise forms.ValidationError(_('There is already invitation for this email.'))
471 except Invitation.DoesNotExist:
475 class ExtendedPasswordChangeForm(PasswordChangeForm):
477 Extends PasswordChangeForm by enabling user
478 to optionally renew also the token.
480 renew = forms.BooleanField(label='Renew token', required=False)
482 def __init__(self, user, *args, **kwargs):
483 super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
485 def save(self, commit=True):
486 user = super(ExtendedPasswordChangeForm, self).save(commit=False)
487 if self.cleaned_data.get('renew'):
493 class AstakosGroupCreationForm(forms.ModelForm):
494 # issue_date = forms.DateField(widget=SelectDateWidget())
495 # expiration_date = forms.DateField(widget=SelectDateWidget())
496 kind = forms.ModelChoiceField(
497 queryset=GroupKind.objects.all(),
499 widget=forms.HiddenInput()
501 name = forms.URLField()
506 def __init__(self, *args, **kwargs):
508 resources = kwargs.pop('resources')
511 super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
512 self.fields.keyOrder = ['kind', 'name', 'desc', 'issue_date',
513 'expiration_date', 'estimated_participants',
514 'moderation_enabled']
515 for id, r in resources.iteritems():
516 self.fields['resource_%s' % id] = forms.IntegerField(
519 help_text=_('Leave it blank for no additional quota.')
523 for name, value in self.cleaned_data.items():
524 prefix, delimiter, suffix = name.partition('resource_')
526 # yield only those having a value
529 yield (suffix, value)
531 class AstakosGroupSearchForm(forms.Form):
532 q = forms.CharField(max_length=200, label='')