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, PasswordResetForm
39 from django.core.mail import send_mail
40 from django.contrib.auth.tokens import default_token_generator
41 from django.template import Context, loader
42 from django.utils.http import int_to_base36
43 from django.core.urlresolvers import reverse
44 from django.utils.functional import lazy
45 from django.utils.safestring import mark_safe
47 from astakos.im.models import AstakosUser, Invitation
48 from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, RECAPTCHA_ENABLED
49 from astakos.im.widgets import DummyWidget, RecaptchaWidget, ApprovalTermsWidget
51 # since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
52 from astakos.im.util import reverse_lazy, get_latest_terms
55 import recaptcha.client.captcha as captcha
57 logger = logging.getLogger(__name__)
59 class LocalUserCreationForm(UserCreationForm):
61 Extends the built in UserCreationForm in several ways:
63 * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
64 * The username field isn't visible and it is assigned a generated id.
65 * User created is not active.
67 recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
68 recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
72 fields = ("email", "first_name", "last_name", "has_signed_terms")
73 widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
75 def __init__(self, *args, **kwargs):
77 Changes the order of fields, and removes the username field.
80 self.ip = kwargs['ip']
82 super(LocalUserCreationForm, self).__init__(*args, **kwargs)
83 self.fields.keyOrder = ['email', 'first_name', 'last_name',
84 'password1', 'password2']
85 if get_latest_terms():
86 self.fields.keyOrder.append('has_signed_terms')
88 self.fields.keyOrder.extend(['recaptcha_challenge_field',
89 'recaptcha_response_field',])
91 if 'has_signed_terms' in self.fields:
92 # Overriding field label since we need to apply a link
93 # to the terms within the label
94 terms_link_html = '<a href="%s" target="_blank">%s</a>' \
95 % (reverse('latest_terms'), _("the terms"))
96 self.fields['has_signed_terms'].label = \
97 mark_safe("I agree with %s" % terms_link_html)
99 def clean_email(self):
100 email = self.cleaned_data['email']
102 raise forms.ValidationError(_("This field is required"))
104 AstakosUser.objects.get(email = email)
105 raise forms.ValidationError(_("This email is already used"))
106 except AstakosUser.DoesNotExist:
109 def clean_has_signed_terms(self):
110 has_signed_terms = self.cleaned_data['has_signed_terms']
111 if not has_signed_terms:
112 raise forms.ValidationError(_('You have to agree with the terms'))
113 return has_signed_terms
115 def clean_recaptcha_response_field(self):
116 if 'recaptcha_challenge_field' in self.cleaned_data:
117 self.validate_captcha()
118 return self.cleaned_data['recaptcha_response_field']
120 def clean_recaptcha_challenge_field(self):
121 if 'recaptcha_response_field' in self.cleaned_data:
122 self.validate_captcha()
123 return self.cleaned_data['recaptcha_challenge_field']
125 def validate_captcha(self):
126 rcf = self.cleaned_data['recaptcha_challenge_field']
127 rrf = self.cleaned_data['recaptcha_response_field']
128 check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
129 if not check.is_valid:
130 raise forms.ValidationError(_('You have not entered the correct words'))
132 def save(self, commit=True):
134 Saves the email, first_name and last_name properties, after the normal
135 save behavior is complete.
137 user = super(LocalUserCreationForm, self).save(commit=False)
141 logger.info('Created user %s', user)
144 class InvitedLocalUserCreationForm(LocalUserCreationForm):
146 Extends the LocalUserCreationForm: adds an inviter readonly field.
149 inviter = forms.CharField(widget=forms.TextInput(), label=_('Inviter Real Name'))
153 fields = ("email", "first_name", "last_name", "has_signed_terms")
154 widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
156 def __init__(self, *args, **kwargs):
158 Changes the order of fields, and removes the username field.
160 super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
162 #set readonly form fields
163 ro = ('inviter', 'email', 'username',)
165 self.fields[f].widget.attrs['readonly'] = True
168 def save(self, commit=True):
169 user = super(InvitedLocalUserCreationForm, self).save(commit=False)
170 level = user.invitation.inviter.level + 1
172 user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
173 user.email_verified = True
178 class ThirdPartyUserCreationForm(forms.ModelForm):
181 fields = ("email", "first_name", "last_name", "third_party_identifier",
182 "has_signed_terms", "provider")
183 widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
185 def __init__(self, *args, **kwargs):
187 Changes the order of fields, and removes the username field.
191 super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
192 self.fields.keyOrder = ['email', 'first_name', 'last_name',
193 'provider', 'third_party_identifier']
194 if get_latest_terms():
195 self.fields.keyOrder.append('has_signed_terms')
196 #set readonly form fields
197 ro = ["provider", "third_party_identifier", "first_name", "last_name"]
199 self.fields[f].widget.attrs['readonly'] = True
201 if 'has_signed_terms' in self.fields:
202 # Overriding field label since we need to apply a link
203 # to the terms within the label
204 terms_link_html = '<a href="%s" target="_blank">%s</a>' \
205 % (reverse('latest_terms'), _("the terms"))
206 self.fields['has_signed_terms'].label = \
207 mark_safe("I agree with %s" % terms_link_html)
209 def clean_email(self):
210 email = self.cleaned_data['email']
212 raise forms.ValidationError(_("This field is required"))
214 AstakosUser.objects.get(email = email)
215 raise forms.ValidationError(_("This email is already used"))
216 except AstakosUser.DoesNotExist:
219 def clean_has_signed_terms(self):
220 has_signed_terms = self.cleaned_data['has_signed_terms']
221 if not has_signed_terms:
222 raise forms.ValidationError(_('You have to agree with the terms'))
223 return has_signed_terms
225 def save(self, commit=True):
226 user = super(ThirdPartyUserCreationForm, self).save(commit=False)
227 user.set_unusable_password()
231 logger.info('Created user %s', user)
234 #class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
235 # def __init__(self, *args, **kwargs):
236 # super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
237 # #set readonly form fields
238 # self.fields['email'].widget.attrs['readonly'] = True
240 class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
242 Extends the LocalUserCreationForm: adds an inviter readonly field.
244 inviter = forms.CharField(widget=forms.TextInput(), label=_('Inviter Real Name'))
246 def __init__(self, *args, **kwargs):
248 Changes the order of fields, and removes the username field.
250 super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
252 #set readonly form fields
253 ro = ('inviter', 'email',)
255 self.fields[f].widget.attrs['readonly'] = True
257 def save(self, commit=True):
258 user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
259 level = user.invitation.inviter.level + 1
261 user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
262 user.email_verified = True
267 class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
268 def clean_email(self):
269 email = self.cleaned_data['email']
271 raise forms.ValidationError(_("This field is required"))
273 user = AstakosUser.objects.get(email = email)
274 if user.provider == 'local':
278 raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
279 except AstakosUser.DoesNotExist:
282 class InvitedShibbolethUserCreationForm(InvitedThirdPartyUserCreationForm):
285 class LoginForm(AuthenticationForm):
286 username = forms.EmailField(label=_("Email"))
288 class ProfileForm(forms.ModelForm):
290 Subclass of ``ModelForm`` for permiting user to edit his/her profile.
291 Most of the fields are readonly since the user is not allowed to change them.
293 The class defines a save method which sets ``is_verified`` to True so as the user
294 during the next login will not to be redirected to profile page.
296 renew = forms.BooleanField(label='Renew token', required=False)
300 fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires', 'groups')
302 def __init__(self, *args, **kwargs):
303 super(ProfileForm, self).__init__(*args, **kwargs)
304 instance = getattr(self, 'instance', None)
305 ro_fields = ('auth_token', 'auth_token_expires', 'groups')
306 if instance and instance.id:
307 for field in ro_fields:
308 self.fields[field].widget.attrs['readonly'] = True
310 def save(self, commit=True):
311 user = super(ProfileForm, self).save(commit=False)
312 user.is_verified = True
313 if self.cleaned_data.get('renew'):
319 class FeedbackForm(forms.Form):
321 Form for writing feedback.
323 feedback_msg = forms.CharField(widget=forms.TextInput(), label=u'Message')
324 feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
327 class SendInvitationForm(forms.Form):
329 Form for sending an invitations
332 email = forms.EmailField(required = True, label = 'Email address')
333 first_name = forms.EmailField(label = 'First name')
334 last_name = forms.EmailField(label = 'Last name')
336 class ExtendedPasswordResetForm(PasswordResetForm):
338 Extends PasswordResetForm by overriding save method:
339 passes a custom from_email in send_mail.
341 Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
342 accepts a from_email argument.
344 def clean_email(self):
345 email = super(ExtendedPasswordResetForm, self).clean_email()
347 user = AstakosUser.objects.get(email=email)
348 if not user.has_usable_password():
349 raise forms.ValidationError(_("This account has not a usable password."))
350 except AstakosUser.DoesNotExist, e:
351 raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
354 def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
355 use_https=False, token_generator=default_token_generator, request=None):
357 Generates a one-use only link for resetting password and sends to the user.
359 for user in self.users_cache:
360 url = reverse('django.contrib.auth.views.password_reset_confirm',
361 kwargs={'uidb36':int_to_base36(user.id),
362 'token':token_generator.make_token(user)})
363 url = request.build_absolute_uri(url)
364 t = loader.get_template(email_template_name)
368 'site_name': SITENAME,
370 'baseurl': request.build_absolute_uri(),
371 'support': DEFAULT_CONTACT_EMAIL
373 from_email = DEFAULT_FROM_EMAIL
374 send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
375 t.render(Context(c)), from_email, [user.email])
377 class SignApprovalTermsForm(forms.ModelForm):
380 fields = ("has_signed_terms",)
382 def __init__(self, *args, **kwargs):
383 super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
385 def clean_has_signed_terms(self):
386 has_signed_terms = self.cleaned_data['has_signed_terms']
387 if not has_signed_terms:
388 raise forms.ValidationError(_('You have to agree with the terms'))
389 return has_signed_terms
391 class InvitationForm(forms.ModelForm):
392 username = forms.EmailField(label=_("Email"))
394 def __init__(self, *args, **kwargs):
395 super(InvitationForm, self).__init__(*args, **kwargs)
399 fields = ('username', 'realname')
401 def clean_username(self):
402 username = self.cleaned_data['username']
404 Invitation.objects.get(username = username)
405 raise forms.ValidationError(_('There is already invitation for this email.'))
406 except Invitation.DoesNotExist: