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,
59 from astakos.im.widgets import DummyWidget, RecaptchaWidget
60 from astakos.im.functions import send_change_email
62 from astakos.im.util import reserved_email, get_query
66 import recaptcha.client.captcha as captcha
67 from random import random
69 logger = logging.getLogger(__name__)
72 class LocalUserCreationForm(UserCreationForm):
74 Extends the built in UserCreationForm in several ways:
76 * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
77 * The username field isn't visible and it is assigned a generated id.
78 * User created is not active.
80 recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
81 recaptcha_response_field = forms.CharField(
82 widget=RecaptchaWidget, label='')
86 fields = ("email", "first_name", "last_name",
87 "has_signed_terms", "has_signed_terms")
89 def __init__(self, *args, **kwargs):
91 Changes the order of fields, and removes the username field.
93 request = kwargs.get('request', None)
96 self.ip = request.META.get('REMOTE_ADDR',
97 request.META.get('HTTP_X_REAL_IP', None))
99 super(LocalUserCreationForm, self).__init__(*args, **kwargs)
100 self.fields.keyOrder = ['email', 'first_name', 'last_name',
101 'password1', 'password2']
103 if RECAPTCHA_ENABLED:
104 self.fields.keyOrder.extend(['recaptcha_challenge_field',
105 'recaptcha_response_field', ])
106 if get_latest_terms():
107 self.fields.keyOrder.append('has_signed_terms')
109 if 'has_signed_terms' in self.fields:
110 # Overriding field label since we need to apply a link
111 # to the terms within the label
112 terms_link_html = '<a href="%s" target="_blank">%s</a>' \
113 % (reverse('latest_terms'), _("the terms"))
114 self.fields['has_signed_terms'].label = \
115 mark_safe("I agree with %s" % terms_link_html)
117 def clean_email(self):
118 email = self.cleaned_data['email']
120 raise forms.ValidationError(_("This field is required"))
121 if reserved_email(email):
122 raise forms.ValidationError(_("This email is already used"))
125 def clean_has_signed_terms(self):
126 has_signed_terms = self.cleaned_data['has_signed_terms']
127 if not has_signed_terms:
128 raise forms.ValidationError(_('You have to agree with the terms'))
129 return has_signed_terms
131 def clean_recaptcha_response_field(self):
132 if 'recaptcha_challenge_field' in self.cleaned_data:
133 self.validate_captcha()
134 return self.cleaned_data['recaptcha_response_field']
136 def clean_recaptcha_challenge_field(self):
137 if 'recaptcha_response_field' in self.cleaned_data:
138 self.validate_captcha()
139 return self.cleaned_data['recaptcha_challenge_field']
141 def validate_captcha(self):
142 rcf = self.cleaned_data['recaptcha_challenge_field']
143 rrf = self.cleaned_data['recaptcha_response_field']
144 check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
145 if not check.is_valid:
146 raise forms.ValidationError(
147 _('You have not entered the correct words'))
149 def save(self, commit=True):
151 Saves the email, first_name and last_name properties, after the normal
152 save behavior is complete.
154 user = super(LocalUserCreationForm, self).save(commit=False)
158 logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
162 class InvitedLocalUserCreationForm(LocalUserCreationForm):
164 Extends the LocalUserCreationForm: email is readonly.
168 fields = ("email", "first_name", "last_name", "has_signed_terms")
170 def __init__(self, *args, **kwargs):
172 Changes the order of fields, and removes the username field.
174 super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
176 #set readonly form fields
177 ro = ('email', 'username',)
179 self.fields[f].widget.attrs['readonly'] = True
181 def save(self, commit=True):
182 user = super(InvitedLocalUserCreationForm, self).save(commit=False)
183 level = user.invitation.inviter.level + 1
185 user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
186 user.email_verified = True
192 class ThirdPartyUserCreationForm(forms.ModelForm):
195 fields = ("email", "first_name", "last_name",
196 "third_party_identifier", "has_signed_terms")
198 def __init__(self, *args, **kwargs):
200 Changes the order of fields, and removes the username field.
202 self.request = kwargs.get('request', None)
204 kwargs.pop('request')
205 super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
206 self.fields.keyOrder = ['email', 'first_name', 'last_name',
207 'third_party_identifier']
208 if get_latest_terms():
209 self.fields.keyOrder.append('has_signed_terms')
210 #set readonly form fields
211 ro = ["third_party_identifier"]
213 self.fields[f].widget.attrs['readonly'] = True
215 if 'has_signed_terms' in self.fields:
216 # Overriding field label since we need to apply a link
217 # to the terms within the label
218 terms_link_html = '<a href="%s" target="_blank">%s</a>' \
219 % (reverse('latest_terms'), _("the terms"))
220 self.fields['has_signed_terms'].label = \
221 mark_safe("I agree with %s" % terms_link_html)
223 def clean_email(self):
224 email = self.cleaned_data['email']
226 raise forms.ValidationError(_("This field is required"))
229 def clean_has_signed_terms(self):
230 has_signed_terms = self.cleaned_data['has_signed_terms']
231 if not has_signed_terms:
232 raise forms.ValidationError(_('You have to agree with the terms'))
233 return has_signed_terms
235 def save(self, commit=True):
236 user = super(ThirdPartyUserCreationForm, self).save(commit=False)
237 user.set_unusable_password()
239 user.provider = get_query(self.request).get('provider')
242 logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
246 class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
248 Extends the ThirdPartyUserCreationForm: email is readonly.
250 def __init__(self, *args, **kwargs):
252 Changes the order of fields, and removes the username field.
255 InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
257 #set readonly form fields
260 self.fields[f].widget.attrs['readonly'] = True
262 def save(self, commit=True):
264 InvitedThirdPartyUserCreationForm, self).save(commit=False)
265 level = user.invitation.inviter.level + 1
267 user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
268 user.email_verified = True
274 class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
275 additional_email = forms.CharField(
276 widget=forms.HiddenInput(), label='', required=False)
278 def __init__(self, *args, **kwargs):
279 super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
280 self.fields.keyOrder.append('additional_email')
281 # copy email value to additional_mail in case user will change it
283 field = self.fields[name]
284 self.initial['additional_email'] = self.initial.get(name,
287 def clean_email(self):
288 email = self.cleaned_data['email']
289 for user in AstakosUser.objects.filter(email=email):
290 if user.provider == 'shibboleth':
291 raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
292 elif not user.is_active:
293 raise forms.ValidationError(_("This email is already associated with an inactive account. \
294 You need to wait to be activated before being able to switch to a shibboleth account."))
295 super(ShibbolethUserCreationForm, self).clean_email()
299 class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
303 class LoginForm(AuthenticationForm):
304 username = forms.EmailField(label=_("Email"))
305 recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
306 recaptcha_response_field = forms.CharField(
307 widget=RecaptchaWidget, label='')
309 def __init__(self, *args, **kwargs):
310 was_limited = kwargs.get('was_limited', False)
311 request = kwargs.get('request', None)
313 self.ip = request.META.get('REMOTE_ADDR',
314 request.META.get('HTTP_X_REAL_IP', None))
316 t = ('request', 'was_limited')
318 if elem in kwargs.keys():
320 super(LoginForm, self).__init__(*args, **kwargs)
322 self.fields.keyOrder = ['username', 'password']
323 if was_limited and RECAPTCHA_ENABLED:
324 self.fields.keyOrder.extend(['recaptcha_challenge_field',
325 'recaptcha_response_field', ])
327 def clean_recaptcha_response_field(self):
328 if 'recaptcha_challenge_field' in self.cleaned_data:
329 self.validate_captcha()
330 return self.cleaned_data['recaptcha_response_field']
332 def clean_recaptcha_challenge_field(self):
333 if 'recaptcha_response_field' in self.cleaned_data:
334 self.validate_captcha()
335 return self.cleaned_data['recaptcha_challenge_field']
337 def validate_captcha(self):
338 rcf = self.cleaned_data['recaptcha_challenge_field']
339 rrf = self.cleaned_data['recaptcha_response_field']
340 check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
341 if not check.is_valid:
342 raise forms.ValidationError(
343 _('You have not entered the correct words'))
346 super(LoginForm, self).clean()
347 if self.user_cache and self.user_cache.provider not in ('local', ''):
348 raise forms.ValidationError(_('Local login is not the current authentication method for this account.'))
349 return self.cleaned_data
352 class ProfileForm(forms.ModelForm):
354 Subclass of ``ModelForm`` for permiting user to edit his/her profile.
355 Most of the fields are readonly since the user is not allowed to change them.
357 The class defines a save method which sets ``is_verified`` to True so as the user
358 during the next login will not to be redirected to profile page.
360 renew = forms.BooleanField(label='Renew token', required=False)
364 fields = ('email', 'first_name', 'last_name', 'auth_token',
365 'auth_token_expires')
367 def __init__(self, *args, **kwargs):
368 super(ProfileForm, self).__init__(*args, **kwargs)
369 instance = getattr(self, 'instance', None)
370 ro_fields = ('email', 'auth_token', 'auth_token_expires')
371 if instance and instance.id:
372 for field in ro_fields:
373 self.fields[field].widget.attrs['readonly'] = True
375 def save(self, commit=True):
376 user = super(ProfileForm, self).save(commit=False)
377 user.is_verified = True
378 if self.cleaned_data.get('renew'):
385 class FeedbackForm(forms.Form):
387 Form for writing feedback.
389 feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
390 feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
394 class SendInvitationForm(forms.Form):
396 Form for sending an invitations
399 email = forms.EmailField(required=True, label='Email address')
400 first_name = forms.EmailField(label='First name')
401 last_name = forms.EmailField(label='Last name')
404 class ExtendedPasswordResetForm(PasswordResetForm):
406 Extends PasswordResetForm by overriding save method:
407 passes a custom from_email in send_mail.
409 Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
410 accepts a from_email argument.
412 def clean_email(self):
413 email = super(ExtendedPasswordResetForm, self).clean_email()
415 user = AstakosUser.objects.get(email=email, is_active=True)
416 if not user.has_usable_password():
417 raise forms.ValidationError(
418 _("This account has not a usable password."))
419 except AstakosUser.DoesNotExist:
420 raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
424 self, domain_override=None, email_template_name='registration/password_reset_email.html',
425 use_https=False, token_generator=default_token_generator, request=None):
427 Generates a one-use only link for resetting password and sends to the user.
429 for user in self.users_cache:
430 url = reverse('django.contrib.auth.views.password_reset_confirm',
431 kwargs={'uidb36': int_to_base36(user.id),
432 'token': token_generator.make_token(user)
435 url = urljoin(BASEURL, url)
436 t = loader.get_template(email_template_name)
440 'site_name': SITENAME,
443 'support': DEFAULT_CONTACT_EMAIL
445 from_email = settings.SERVER_EMAIL
446 send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
447 t.render(Context(c)), from_email, [user.email])
450 class EmailChangeForm(forms.ModelForm):
453 fields = ('new_email_address',)
455 def clean_new_email_address(self):
456 addr = self.cleaned_data['new_email_address']
457 if AstakosUser.objects.filter(email__iexact=addr):
458 raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
461 def save(self, email_template_name, request, commit=True):
462 ec = super(EmailChangeForm, self).save(commit=False)
463 ec.user = request.user
464 activation_key = hashlib.sha1(
465 str(random()) + smart_str(ec.new_email_address))
466 ec.activation_key = activation_key.hexdigest()
469 send_change_email(ec, request, email_template_name=email_template_name)
472 class SignApprovalTermsForm(forms.ModelForm):
475 fields = ("has_signed_terms",)
477 def __init__(self, *args, **kwargs):
478 super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
480 def clean_has_signed_terms(self):
481 has_signed_terms = self.cleaned_data['has_signed_terms']
482 if not has_signed_terms:
483 raise forms.ValidationError(_('You have to agree with the terms'))
484 return has_signed_terms
487 class InvitationForm(forms.ModelForm):
488 username = forms.EmailField(label=_("Email"))
490 def __init__(self, *args, **kwargs):
491 super(InvitationForm, self).__init__(*args, **kwargs)
495 fields = ('username', 'realname')
497 def clean_username(self):
498 username = self.cleaned_data['username']
500 Invitation.objects.get(username=username)
501 raise forms.ValidationError(
502 _('There is already invitation for this email.'))
503 except Invitation.DoesNotExist:
508 class ExtendedPasswordChangeForm(PasswordChangeForm):
510 Extends PasswordChangeForm by enabling user
511 to optionally renew also the token.
513 renew = forms.BooleanField(label='Renew token', required=False)
515 def __init__(self, user, *args, **kwargs):
516 super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
518 def save(self, commit=True):
519 user = super(ExtendedPasswordChangeForm, self).save(commit=False)
520 if self.cleaned_data.get('renew'):
527 class AstakosGroupCreationForm(forms.ModelForm):
528 # issue_date = forms.DateField(widget=SelectDateWidget())
529 # expiration_date = forms.DateField(widget=SelectDateWidget())
530 kind = forms.ModelChoiceField(
531 queryset=GroupKind.objects.all(),
533 widget=forms.HiddenInput()
535 name = 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)
569 class AstakosGroupUpdateForm(forms.ModelForm):
572 fields = ('homepage', 'desc')
574 class AddGroupMembersForm(forms.Form):
575 q = forms.CharField(max_length=800, widget=forms.Textarea, label=_('Search users'),
576 help_text=_('Add comma separated user emails'),
580 q = self.cleaned_data.get('q') or ''
582 users = list(u.strip() for u in users if u)
583 db_entries = AstakosUser.objects.filter(email__in=users)
584 unknown = list(set(users) - set(u.email for u in db_entries))
586 raise forms.ValidationError(
587 _('Unknown users: %s' % unknown))
588 self.valid_users = db_entries
589 return self.cleaned_data
591 def get_valid_users(self):
592 """Should be called after form cleaning"""
594 return self.valid_users
599 class AstakosGroupSearchForm(forms.Form):
600 q = forms.CharField(max_length=200, label='Search group')