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
35 from django import forms
36 from django.utils.translation import ugettext as _
37 from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm,
38 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.safestring import mark_safe
46 from django.utils.encoding import smart_str
47 from django.forms.extras.widgets import SelectDateWidget
48 from django.conf import settings
50 from astakos.im.models import (
51 AstakosUser, EmailChange, AstakosGroup, Invitation,
52 Membership, GroupKind, get_latest_terms
54 from astakos.im.settings import (INVITATIONS_PER_LEVEL, BASEURL, SITENAME,
55 RECAPTCHA_PRIVATE_KEY, RECAPTCHA_ENABLED, DEFAULT_CONTACT_EMAIL,
58 from astakos.im.widgets import DummyWidget, RecaptchaWidget
59 from astakos.im.functions import send_change_email
61 from astakos.im.util import reserved_email, get_query
65 import recaptcha.client.captcha as captcha
66 from random import random
68 logger = logging.getLogger(__name__)
71 class LocalUserCreationForm(UserCreationForm):
73 Extends the built in UserCreationForm in several ways:
75 * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
76 * The username field isn't visible and it is assigned a generated id.
77 * User created is not active.
79 recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
80 recaptcha_response_field = forms.CharField(
81 widget=RecaptchaWidget, label='')
85 fields = ("email", "first_name", "last_name",
86 "has_signed_terms", "has_signed_terms")
88 def __init__(self, *args, **kwargs):
90 Changes the order of fields, and removes the username field.
92 request = kwargs.get('request', None)
95 self.ip = request.META.get('REMOTE_ADDR',
96 request.META.get('HTTP_X_REAL_IP', None))
98 super(LocalUserCreationForm, self).__init__(*args, **kwargs)
99 self.fields.keyOrder = ['email', 'first_name', 'last_name',
100 'password1', 'password2']
102 if RECAPTCHA_ENABLED:
103 self.fields.keyOrder.extend(['recaptcha_challenge_field',
104 'recaptcha_response_field', ])
105 if get_latest_terms():
106 self.fields.keyOrder.append('has_signed_terms')
108 if 'has_signed_terms' in self.fields:
109 # Overriding field label since we need to apply a link
110 # to the terms within the label
111 terms_link_html = '<a href="%s" target="_blank">%s</a>' \
112 % (reverse('latest_terms'), _("the terms"))
113 self.fields['has_signed_terms'].label = \
114 mark_safe("I agree with %s" % terms_link_html)
116 def clean_email(self):
117 email = self.cleaned_data['email']
119 raise forms.ValidationError(_("This field is required"))
120 if reserved_email(email):
121 raise forms.ValidationError(_("This email is already used"))
124 def clean_has_signed_terms(self):
125 has_signed_terms = self.cleaned_data['has_signed_terms']
126 if not has_signed_terms:
127 raise forms.ValidationError(_('You have to agree with the terms'))
128 return has_signed_terms
130 def clean_recaptcha_response_field(self):
131 if 'recaptcha_challenge_field' in self.cleaned_data:
132 self.validate_captcha()
133 return self.cleaned_data['recaptcha_response_field']
135 def clean_recaptcha_challenge_field(self):
136 if 'recaptcha_response_field' in self.cleaned_data:
137 self.validate_captcha()
138 return self.cleaned_data['recaptcha_challenge_field']
140 def validate_captcha(self):
141 rcf = self.cleaned_data['recaptcha_challenge_field']
142 rrf = self.cleaned_data['recaptcha_response_field']
143 check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
144 if not check.is_valid:
145 raise forms.ValidationError(
146 _('You have not entered the correct words'))
148 def save(self, commit=True):
150 Saves the email, first_name and last_name properties, after the normal
151 save behavior is complete.
153 user = super(LocalUserCreationForm, self).save(commit=False)
157 logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
161 class InvitedLocalUserCreationForm(LocalUserCreationForm):
163 Extends the LocalUserCreationForm: email is readonly.
167 fields = ("email", "first_name", "last_name", "has_signed_terms")
169 def __init__(self, *args, **kwargs):
171 Changes the order of fields, and removes the username field.
173 super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
175 #set readonly form fields
176 ro = ('email', 'username',)
178 self.fields[f].widget.attrs['readonly'] = True
180 def save(self, commit=True):
181 user = super(InvitedLocalUserCreationForm, self).save(commit=False)
182 level = user.invitation.inviter.level + 1
184 user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
185 user.email_verified = True
191 class ThirdPartyUserCreationForm(forms.ModelForm):
194 fields = ("email", "first_name", "last_name",
195 "third_party_identifier", "has_signed_terms")
197 def __init__(self, *args, **kwargs):
199 Changes the order of fields, and removes the username field.
201 self.request = kwargs.get('request', None)
203 kwargs.pop('request')
204 super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
205 self.fields.keyOrder = ['email', 'first_name', 'last_name',
206 'third_party_identifier']
207 if get_latest_terms():
208 self.fields.keyOrder.append('has_signed_terms')
209 #set readonly form fields
210 ro = ["third_party_identifier"]
212 self.fields[f].widget.attrs['readonly'] = True
214 if 'has_signed_terms' in self.fields:
215 # Overriding field label since we need to apply a link
216 # to the terms within the label
217 terms_link_html = '<a href="%s" target="_blank">%s</a>' \
218 % (reverse('latest_terms'), _("the terms"))
219 self.fields['has_signed_terms'].label = \
220 mark_safe("I agree with %s" % terms_link_html)
222 def clean_email(self):
223 email = self.cleaned_data['email']
225 raise forms.ValidationError(_("This field is required"))
228 def clean_has_signed_terms(self):
229 has_signed_terms = self.cleaned_data['has_signed_terms']
230 if not has_signed_terms:
231 raise forms.ValidationError(_('You have to agree with the terms'))
232 return has_signed_terms
234 def save(self, commit=True):
235 user = super(ThirdPartyUserCreationForm, self).save(commit=False)
236 user.set_unusable_password()
238 user.provider = get_query(self.request).get('provider')
241 logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
245 class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
247 Extends the ThirdPartyUserCreationForm: email is readonly.
249 def __init__(self, *args, **kwargs):
251 Changes the order of fields, and removes the username field.
254 InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
256 #set readonly form fields
259 self.fields[f].widget.attrs['readonly'] = True
261 def save(self, commit=True):
263 InvitedThirdPartyUserCreationForm, self).save(commit=False)
264 level = user.invitation.inviter.level + 1
266 user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
267 user.email_verified = True
273 class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
274 additional_email = forms.CharField(
275 widget=forms.HiddenInput(), label='', required=False)
277 def __init__(self, *args, **kwargs):
278 super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
279 self.fields.keyOrder.append('additional_email')
280 # copy email value to additional_mail in case user will change it
282 field = self.fields[name]
283 self.initial['additional_email'] = self.initial.get(name,
286 def clean_email(self):
287 email = self.cleaned_data['email']
288 for user in AstakosUser.objects.filter(email=email):
289 if user.provider == 'shibboleth':
290 raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
291 elif not user.is_active:
292 raise forms.ValidationError(_("This email is already associated with an inactive account. \
293 You need to wait to be activated before being able to switch to a shibboleth account."))
294 super(ShibbolethUserCreationForm, self).clean_email()
298 class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
302 class LoginForm(AuthenticationForm):
303 username = forms.EmailField(label=_("Email"))
304 recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
305 recaptcha_response_field = forms.CharField(
306 widget=RecaptchaWidget, label='')
308 def __init__(self, *args, **kwargs):
309 was_limited = kwargs.get('was_limited', False)
310 request = kwargs.get('request', None)
312 self.ip = request.META.get('REMOTE_ADDR',
313 request.META.get('HTTP_X_REAL_IP', None))
315 t = ('request', 'was_limited')
317 if elem in kwargs.keys():
319 super(LoginForm, self).__init__(*args, **kwargs)
321 self.fields.keyOrder = ['username', 'password']
322 if was_limited and RECAPTCHA_ENABLED:
323 self.fields.keyOrder.extend(['recaptcha_challenge_field',
324 'recaptcha_response_field', ])
326 def clean_recaptcha_response_field(self):
327 if 'recaptcha_challenge_field' in self.cleaned_data:
328 self.validate_captcha()
329 return self.cleaned_data['recaptcha_response_field']
331 def clean_recaptcha_challenge_field(self):
332 if 'recaptcha_response_field' in self.cleaned_data:
333 self.validate_captcha()
334 return self.cleaned_data['recaptcha_challenge_field']
336 def validate_captcha(self):
337 rcf = self.cleaned_data['recaptcha_challenge_field']
338 rrf = self.cleaned_data['recaptcha_response_field']
339 check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
340 if not check.is_valid:
341 raise forms.ValidationError(
342 _('You have not entered the correct words'))
345 super(LoginForm, self).clean()
346 if self.user_cache and self.user_cache.provider not in ('local', ''):
347 raise forms.ValidationError(_('Local login is not the current authentication method for this account.'))
348 return self.cleaned_data
351 class ProfileForm(forms.ModelForm):
353 Subclass of ``ModelForm`` for permiting user to edit his/her profile.
354 Most of the fields are readonly since the user is not allowed to change them.
356 The class defines a save method which sets ``is_verified`` to True so as the user
357 during the next login will not to be redirected to profile page.
359 renew = forms.BooleanField(label='Renew token', required=False)
363 fields = ('email', 'first_name', 'last_name', 'auth_token',
364 'auth_token_expires')
366 def __init__(self, *args, **kwargs):
367 super(ProfileForm, self).__init__(*args, **kwargs)
368 instance = getattr(self, 'instance', None)
369 ro_fields = ('email', 'auth_token', 'auth_token_expires')
370 if instance and instance.id:
371 for field in ro_fields:
372 self.fields[field].widget.attrs['readonly'] = True
374 def save(self, commit=True):
375 user = super(ProfileForm, self).save(commit=False)
376 user.is_verified = True
377 if self.cleaned_data.get('renew'):
384 class FeedbackForm(forms.Form):
386 Form for writing feedback.
388 feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
389 feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
393 class SendInvitationForm(forms.Form):
395 Form for sending an invitations
398 email = forms.EmailField(required=True, label='Email address')
399 first_name = forms.EmailField(label='First name')
400 last_name = forms.EmailField(label='Last name')
403 class ExtendedPasswordResetForm(PasswordResetForm):
405 Extends PasswordResetForm by overriding save method:
406 passes a custom from_email in send_mail.
408 Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
409 accepts a from_email argument.
411 def clean_email(self):
412 email = super(ExtendedPasswordResetForm, self).clean_email()
414 user = AstakosUser.objects.get(email=email, is_active=True)
415 if not user.has_usable_password():
416 raise forms.ValidationError(
417 _("This account has not a usable password."))
418 except AstakosUser.DoesNotExist:
419 raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
423 self, domain_override=None, email_template_name='registration/password_reset_email.html',
424 use_https=False, token_generator=default_token_generator, request=None):
426 Generates a one-use only link for resetting password and sends to the user.
428 for user in self.users_cache:
429 url = reverse('django.contrib.auth.views.password_reset_confirm',
430 kwargs={'uidb36': int_to_base36(user.id),
431 'token': token_generator.make_token(user)
434 url = urljoin(BASEURL, url)
435 t = loader.get_template(email_template_name)
439 'site_name': SITENAME,
442 'support': DEFAULT_CONTACT_EMAIL
444 from_email = settings.SERVER_EMAIL
445 send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
446 t.render(Context(c)), from_email, [user.email])
449 class EmailChangeForm(forms.ModelForm):
452 fields = ('new_email_address',)
454 def clean_new_email_address(self):
455 addr = self.cleaned_data['new_email_address']
456 if AstakosUser.objects.filter(email__iexact=addr):
457 raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
460 def save(self, email_template_name, request, commit=True):
461 ec = super(EmailChangeForm, self).save(commit=False)
462 ec.user = request.user
463 activation_key = hashlib.sha1(
464 str(random()) + smart_str(ec.new_email_address))
465 ec.activation_key = activation_key.hexdigest()
468 send_change_email(ec, request, email_template_name=email_template_name)
471 class SignApprovalTermsForm(forms.ModelForm):
474 fields = ("has_signed_terms",)
476 def __init__(self, *args, **kwargs):
477 super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
479 def clean_has_signed_terms(self):
480 has_signed_terms = self.cleaned_data['has_signed_terms']
481 if not has_signed_terms:
482 raise forms.ValidationError(_('You have to agree with the terms'))
483 return has_signed_terms
486 class InvitationForm(forms.ModelForm):
487 username = forms.EmailField(label=_("Email"))
489 def __init__(self, *args, **kwargs):
490 super(InvitationForm, self).__init__(*args, **kwargs)
494 fields = ('username', 'realname')
496 def clean_username(self):
497 username = self.cleaned_data['username']
499 Invitation.objects.get(username=username)
500 raise forms.ValidationError(
501 _('There is already invitation for this email.'))
502 except Invitation.DoesNotExist:
507 class ExtendedPasswordChangeForm(PasswordChangeForm):
509 Extends PasswordChangeForm by enabling user
510 to optionally renew also the token.
512 renew = forms.BooleanField(label='Renew token', required=False)
514 def __init__(self, user, *args, **kwargs):
515 super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
517 def save(self, commit=True):
518 user = super(ExtendedPasswordChangeForm, self).save(commit=False)
519 if self.cleaned_data.get('renew'):
526 class AstakosGroupCreationForm(forms.ModelForm):
527 # issue_date = forms.DateField(widget=SelectDateWidget())
528 # expiration_date = forms.DateField(widget=SelectDateWidget())
529 kind = forms.ModelChoiceField(
530 queryset=GroupKind.objects.all(),
532 widget=forms.HiddenInput()
534 name = forms.URLField()
535 homepage = forms.URLField()
536 moderation_enabled = forms.BooleanField(
537 help_text="Check if you want to approve members participation manually",
544 def __init__(self, *args, **kwargs):
546 resources = kwargs.pop('resources')
549 super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
550 self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc', 'issue_date',
551 'expiration_date', 'estimated_participants',
552 'moderation_enabled']
553 for id, r in resources.iteritems():
554 self.fields['resource_%s' % id] = forms.IntegerField(
557 help_text=_('Leave it blank for no additional quota.')
561 for name, value in self.cleaned_data.items():
562 prefix, delimiter, suffix = name.partition('resource_')
564 # yield only those having a value
567 yield (suffix, value)
570 class AstakosGroupSearchForm(forms.Form):
571 q = forms.CharField(max_length=200, label='Search group')