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 (AstakosUser, EmailChange, AstakosGroup,
51 Invitation, Membership, GroupKind, Resource,
53 from astakos.im.settings import (INVITATIONS_PER_LEVEL, BASEURL, SITENAME,
54 RECAPTCHA_PRIVATE_KEY, RECAPTCHA_ENABLED,
55 DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL)
57 from astakos.im.widgets import DummyWidget, RecaptchaWidget
58 from astakos.im.functions import send_change_email
60 from astakos.im.util import reserved_email, get_query
64 import recaptcha.client.captcha as captcha
65 from random import random
67 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(
80 widget=RecaptchaWidget, label='')
84 fields = ("email", "first_name", "last_name",
85 "has_signed_terms", "has_signed_terms")
87 def __init__(self, *args, **kwargs):
89 Changes the order of fields, and removes the username field.
91 request = kwargs.get('request', None)
94 self.ip = request.META.get('REMOTE_ADDR',
95 request.META.get('HTTP_X_REAL_IP', None))
97 super(LocalUserCreationForm, self).__init__(*args, **kwargs)
98 self.fields.keyOrder = ['email', 'first_name', 'last_name',
99 'password1', 'password2']
101 if RECAPTCHA_ENABLED:
102 self.fields.keyOrder.extend(['recaptcha_challenge_field',
103 'recaptcha_response_field', ])
104 if get_latest_terms():
105 self.fields.keyOrder.append('has_signed_terms')
107 if 'has_signed_terms' in self.fields:
108 # Overriding field label since we need to apply a link
109 # to the terms within the label
110 terms_link_html = '<a href="%s" target="_blank">%s</a>' \
111 % (reverse('latest_terms'), _("the terms"))
112 self.fields['has_signed_terms'].label = \
113 mark_safe("I agree with %s" % terms_link_html)
115 def clean_email(self):
116 email = self.cleaned_data['email']
118 raise forms.ValidationError(_("This field is required"))
119 if reserved_email(email):
120 raise forms.ValidationError(_("This email is already used"))
123 def clean_has_signed_terms(self):
124 has_signed_terms = self.cleaned_data['has_signed_terms']
125 if not has_signed_terms:
126 raise forms.ValidationError(_('You have to agree with the terms'))
127 return has_signed_terms
129 def clean_recaptcha_response_field(self):
130 if 'recaptcha_challenge_field' in self.cleaned_data:
131 self.validate_captcha()
132 return self.cleaned_data['recaptcha_response_field']
134 def clean_recaptcha_challenge_field(self):
135 if 'recaptcha_response_field' in self.cleaned_data:
136 self.validate_captcha()
137 return self.cleaned_data['recaptcha_challenge_field']
139 def validate_captcha(self):
140 rcf = self.cleaned_data['recaptcha_challenge_field']
141 rrf = self.cleaned_data['recaptcha_response_field']
142 check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
143 if not check.is_valid:
144 raise forms.ValidationError(
145 _('You have not entered the correct words'))
147 def save(self, commit=True):
149 Saves the email, first_name and last_name properties, after the normal
150 save behavior is complete.
152 user = super(LocalUserCreationForm, self).save(commit=False)
156 logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
160 class InvitedLocalUserCreationForm(LocalUserCreationForm):
162 Extends the LocalUserCreationForm: email is readonly.
166 fields = ("email", "first_name", "last_name", "has_signed_terms")
168 def __init__(self, *args, **kwargs):
170 Changes the order of fields, and removes the username field.
172 super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
174 #set readonly form fields
175 ro = ('email', 'username',)
177 self.fields[f].widget.attrs['readonly'] = True
179 def save(self, commit=True):
180 user = super(InvitedLocalUserCreationForm, self).save(commit=False)
181 level = user.invitation.inviter.level + 1
183 user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
184 user.email_verified = True
190 class ThirdPartyUserCreationForm(forms.ModelForm):
193 fields = ("email", "first_name", "last_name",
194 "third_party_identifier", "has_signed_terms")
196 def __init__(self, *args, **kwargs):
198 Changes the order of fields, and removes the username field.
200 self.request = kwargs.get('request', None)
202 kwargs.pop('request')
203 super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
204 self.fields.keyOrder = ['email', 'first_name', 'last_name',
205 'third_party_identifier']
206 if get_latest_terms():
207 self.fields.keyOrder.append('has_signed_terms')
208 #set readonly form fields
209 ro = ["third_party_identifier"]
211 self.fields[f].widget.attrs['readonly'] = True
213 if 'has_signed_terms' in self.fields:
214 # Overriding field label since we need to apply a link
215 # to the terms within the label
216 terms_link_html = '<a href="%s" target="_blank">%s</a>' \
217 % (reverse('latest_terms'), _("the terms"))
218 self.fields['has_signed_terms'].label = \
219 mark_safe("I agree with %s" % terms_link_html)
221 def clean_email(self):
222 email = self.cleaned_data['email']
224 raise forms.ValidationError(_("This field is required"))
227 def clean_has_signed_terms(self):
228 has_signed_terms = self.cleaned_data['has_signed_terms']
229 if not has_signed_terms:
230 raise forms.ValidationError(_('You have to agree with the terms'))
231 return has_signed_terms
233 def save(self, commit=True):
234 user = super(ThirdPartyUserCreationForm, self).save(commit=False)
235 user.set_unusable_password()
237 user.provider = get_query(self.request).get('provider')
240 logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
244 class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
246 Extends the ThirdPartyUserCreationForm: email is readonly.
248 def __init__(self, *args, **kwargs):
250 Changes the order of fields, and removes the username field.
253 InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
255 #set readonly form fields
258 self.fields[f].widget.attrs['readonly'] = True
260 def save(self, commit=True):
262 InvitedThirdPartyUserCreationForm, self).save(commit=False)
263 level = user.invitation.inviter.level + 1
265 user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
266 user.email_verified = True
272 class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
273 additional_email = forms.CharField(
274 widget=forms.HiddenInput(), label='', required=False)
276 def __init__(self, *args, **kwargs):
277 super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
278 self.fields.keyOrder.append('additional_email')
279 # copy email value to additional_mail in case user will change it
281 field = self.fields[name]
282 self.initial['additional_email'] = self.initial.get(name,
285 def clean_email(self):
286 email = self.cleaned_data['email']
287 for user in AstakosUser.objects.filter(email=email):
288 if user.provider == 'shibboleth':
289 raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
290 elif not user.is_active:
291 raise forms.ValidationError(_("This email is already associated with an inactive account. \
292 You need to wait to be activated before being able to switch to a shibboleth account."))
293 super(ShibbolethUserCreationForm, self).clean_email()
297 class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
301 class LoginForm(AuthenticationForm):
302 username = forms.EmailField(label=_("Email"))
303 recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
304 recaptcha_response_field = forms.CharField(
305 widget=RecaptchaWidget, label='')
307 def __init__(self, *args, **kwargs):
308 was_limited = kwargs.get('was_limited', False)
309 request = kwargs.get('request', None)
311 self.ip = request.META.get('REMOTE_ADDR',
312 request.META.get('HTTP_X_REAL_IP', None))
314 t = ('request', 'was_limited')
316 if elem in kwargs.keys():
318 super(LoginForm, self).__init__(*args, **kwargs)
320 self.fields.keyOrder = ['username', 'password']
321 if was_limited and RECAPTCHA_ENABLED:
322 self.fields.keyOrder.extend(['recaptcha_challenge_field',
323 'recaptcha_response_field', ])
325 def clean_recaptcha_response_field(self):
326 if 'recaptcha_challenge_field' in self.cleaned_data:
327 self.validate_captcha()
328 return self.cleaned_data['recaptcha_response_field']
330 def clean_recaptcha_challenge_field(self):
331 if 'recaptcha_response_field' in self.cleaned_data:
332 self.validate_captcha()
333 return self.cleaned_data['recaptcha_challenge_field']
335 def validate_captcha(self):
336 rcf = self.cleaned_data['recaptcha_challenge_field']
337 rrf = self.cleaned_data['recaptcha_response_field']
338 check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
339 if not check.is_valid:
340 raise forms.ValidationError(
341 _('You have not entered the correct words'))
344 super(LoginForm, self).clean()
345 if self.user_cache and self.user_cache.provider not in ('local', ''):
346 raise forms.ValidationError(_('Local login is not the current authentication method for this account.'))
347 return self.cleaned_data
350 class ProfileForm(forms.ModelForm):
352 Subclass of ``ModelForm`` for permiting user to edit his/her profile.
353 Most of the fields are readonly since the user is not allowed to change them.
355 The class defines a save method which sets ``is_verified`` to True so as the user
356 during the next login will not to be redirected to profile page.
358 renew = forms.BooleanField(label='Renew token', required=False)
362 fields = ('email', 'first_name', 'last_name', 'auth_token',
363 'auth_token_expires')
365 def __init__(self, *args, **kwargs):
366 super(ProfileForm, self).__init__(*args, **kwargs)
367 instance = getattr(self, 'instance', None)
368 ro_fields = ('email', 'auth_token', 'auth_token_expires')
369 if instance and instance.id:
370 for field in ro_fields:
371 self.fields[field].widget.attrs['readonly'] = True
373 def save(self, commit=True):
374 user = super(ProfileForm, self).save(commit=False)
375 user.is_verified = True
376 if self.cleaned_data.get('renew'):
383 class FeedbackForm(forms.Form):
385 Form for writing feedback.
387 feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
388 feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
392 class SendInvitationForm(forms.Form):
394 Form for sending an invitations
397 email = forms.EmailField(required=True, label='Email address')
398 first_name = forms.EmailField(label='First name')
399 last_name = forms.EmailField(label='Last name')
402 class ExtendedPasswordResetForm(PasswordResetForm):
404 Extends PasswordResetForm by overriding save method:
405 passes a custom from_email in send_mail.
407 Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
408 accepts a from_email argument.
410 def clean_email(self):
411 email = super(ExtendedPasswordResetForm, self).clean_email()
413 user = AstakosUser.objects.get(email=email, is_active=True)
414 if not user.has_usable_password():
415 raise forms.ValidationError(
416 _("This account has not a usable password."))
417 except AstakosUser.DoesNotExist:
418 raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
422 self, domain_override=None, email_template_name='registration/password_reset_email.html',
423 use_https=False, token_generator=default_token_generator, request=None):
425 Generates a one-use only link for resetting password and sends to the user.
427 for user in self.users_cache:
428 url = reverse('django.contrib.auth.views.password_reset_confirm',
429 kwargs={'uidb36': int_to_base36(user.id),
430 'token': token_generator.make_token(user)
433 url = urljoin(BASEURL, url)
434 t = loader.get_template(email_template_name)
438 'site_name': SITENAME,
441 'support': DEFAULT_CONTACT_EMAIL
443 from_email = settings.SERVER_EMAIL
444 send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
445 t.render(Context(c)), from_email, [user.email])
448 class EmailChangeForm(forms.ModelForm):
451 fields = ('new_email_address',)
453 def clean_new_email_address(self):
454 addr = self.cleaned_data['new_email_address']
455 if AstakosUser.objects.filter(email__iexact=addr):
456 raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
459 def save(self, email_template_name, request, commit=True):
460 ec = super(EmailChangeForm, self).save(commit=False)
461 ec.user = request.user
462 activation_key = hashlib.sha1(
463 str(random()) + smart_str(ec.new_email_address))
464 ec.activation_key = activation_key.hexdigest()
467 send_change_email(ec, request, email_template_name=email_template_name)
470 class SignApprovalTermsForm(forms.ModelForm):
473 fields = ("has_signed_terms",)
475 def __init__(self, *args, **kwargs):
476 super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
478 def clean_has_signed_terms(self):
479 has_signed_terms = self.cleaned_data['has_signed_terms']
480 if not has_signed_terms:
481 raise forms.ValidationError(_('You have to agree with the terms'))
482 return has_signed_terms
485 class InvitationForm(forms.ModelForm):
486 username = forms.EmailField(label=_("Email"))
488 def __init__(self, *args, **kwargs):
489 super(InvitationForm, self).__init__(*args, **kwargs)
493 fields = ('username', 'realname')
495 def clean_username(self):
496 username = self.cleaned_data['username']
498 Invitation.objects.get(username=username)
499 raise forms.ValidationError(
500 _('There is already invitation for this email.'))
501 except Invitation.DoesNotExist:
506 class ExtendedPasswordChangeForm(PasswordChangeForm):
508 Extends PasswordChangeForm by enabling user
509 to optionally renew also the token.
511 renew = forms.BooleanField(label='Renew token', required=False)
513 def __init__(self, user, *args, **kwargs):
514 super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
516 def save(self, commit=True):
517 user = super(ExtendedPasswordChangeForm, self).save(commit=False)
518 if self.cleaned_data.get('renew'):
525 class AstakosGroupCreationForm(forms.ModelForm):
526 kind = forms.ModelChoiceField(
527 queryset=GroupKind.objects.all(),
529 widget=forms.HiddenInput()
531 name = forms.URLField()
532 moderation_enabled = forms.BooleanField(
533 help_text="Check if you want to approve members participation manually",
540 def __init__(self, *args, **kwargs):
542 resources = kwargs.pop('resources')
545 super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
546 self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc', 'issue_date',
547 'expiration_date', 'estimated_participants',
548 'moderation_enabled']
549 for id, r in resources.iteritems():
550 self.fields['resource_%s' % id] = forms.IntegerField(
553 help_text=_('Leave it blank for no additional quota.')
557 for name, value in self.cleaned_data.items():
558 prefix, delimiter, suffix = name.partition('resource_')
560 # yield only those having a value
563 yield (suffix, value)
565 class AstakosGroupUpdateForm(forms.ModelForm):
568 fields = ('homepage', 'desc')
570 class AddGroupMembersForm(forms.Form):
571 q = forms.CharField(max_length=800, widget=forms.Textarea, label=_('Search users'),
572 help_text=_('Add comma separated user emails'),
576 q = self.cleaned_data.get('q') or ''
578 users = list(u.strip() for u in users if u)
579 db_entries = AstakosUser.objects.filter(email__in=users)
580 unknown = list(set(users) - set(u.email for u in db_entries))
582 raise forms.ValidationError(
583 _('Unknown users: %s' % ','.join(unknown)))
584 self.valid_users = db_entries
585 return self.cleaned_data
587 def get_valid_users(self):
588 """Should be called after form cleaning"""
590 return self.valid_users
595 class AstakosGroupSearchForm(forms.Form):
596 q = forms.CharField(max_length=200, label='Search group')
598 class TimelineForm(forms.Form):
599 # entity = forms.CharField(
600 # widget=forms.HiddenInput(), label='')
601 entity = forms.ModelChoiceField(
602 queryset=AstakosUser.objects.filter(is_active = True)
604 resource = forms.ModelChoiceField(
605 queryset=Resource.objects.all()
607 start_date = forms.DateTimeField()
608 end_date = forms.DateTimeField()
609 details = forms.BooleanField(required=False, label="Detailed Listing")
610 operation = forms.ChoiceField(
611 label = 'Charge Method',
612 choices = ( ('', '-------------'),
613 ('charge_usage', 'Charge Usage'),
614 ('charge_traffic', 'Charge Traffic'), )
617 super(TimelineForm, self).clean()
618 d = self.cleaned_data
620 d['resource'] = str(d['resource'])
621 if 'start_date' in d:
622 d['start_date'] = d['start_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
624 d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
626 d['entity'] = d['entity'].email
629 class AstakosGroupSortForm(forms.Form):
630 sort_by = forms.ChoiceField(label='Sort by',
631 choices=(('groupname', 'Name'), ('kindname', 'Type')),