Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ ef20ea07

History | View | Annotate | Download (20.3 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
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.
15
#
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.
28
#
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
35

    
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, SetPasswordForm
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

    
50
from astakos.im.models import AstakosUser, Invitation, get_latest_terms, EmailChange
51
from astakos.im.settings import (INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL,
52
    BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL,
53
    RECAPTCHA_ENABLED, LOGGING_LEVEL, PASSWORD_RESET_EMAIL_SUBJECT,
54
    NEWPASSWD_INVALIDATE_TOKEN, THIRDPARTY_ACC_ADDITIONAL_FIELDS
55
)
56
from astakos.im.widgets import DummyWidget, RecaptchaWidget
57
from astakos.im.functions import send_change_email
58

    
59
# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
60
from astakos.im.util import reverse_lazy, reserved_email, get_query
61

    
62
import logging
63
import hashlib
64
import recaptcha.client.captcha as captcha
65
from random import random
66

    
67
logger = logging.getLogger(__name__)
68

    
69
class LocalUserCreationForm(UserCreationForm):
70
    """
71
    Extends the built in UserCreationForm in several ways:
72

73
    * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
74
    * The username field isn't visible and it is assigned a generated id.
75
    * User created is not active.
76
    """
77
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
78
    recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
79

    
80
    class Meta:
81
        model = AstakosUser
82
        fields = ("email", "first_name", "last_name", "has_signed_terms", "has_signed_terms")
83

    
84
    def __init__(self, *args, **kwargs):
85
        """
86
        Changes the order of fields, and removes the username field.
87
        """
88
        request = kwargs.get('request', None)
89
        if request:
90
            kwargs.pop('request')
91
            self.ip = request.META.get('REMOTE_ADDR',
92
                                       request.META.get('HTTP_X_REAL_IP', None))
93

    
94
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
95
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
96
                                'password1', 'password2']
97

    
98
        if RECAPTCHA_ENABLED:
99
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
100
                                         'recaptcha_response_field',])
101
        if get_latest_terms():
102
            self.fields.keyOrder.append('has_signed_terms')
103

    
104
        if 'has_signed_terms' in self.fields:
105
            # Overriding field label since we need to apply a link
106
            # to the terms within the label
107
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
108
                    % (reverse('latest_terms'), _("the terms"))
109
            self.fields['has_signed_terms'].label = \
110
                    mark_safe("I agree with %s" % terms_link_html)
111

    
112
    def clean_email(self):
113
        email = self.cleaned_data['email']
114
        if not email:
115
            raise forms.ValidationError(_("This field is required"))
116
        if reserved_email(email):
117
            raise forms.ValidationError(_("This email is already used"))
118
        return email
119

    
120
    def clean_has_signed_terms(self):
121
        has_signed_terms = self.cleaned_data['has_signed_terms']
122
        if not has_signed_terms:
123
            raise forms.ValidationError(_('You have to agree with the terms'))
124
        return has_signed_terms
125

    
126
    def clean_recaptcha_response_field(self):
127
        if 'recaptcha_challenge_field' in self.cleaned_data:
128
            self.validate_captcha()
129
        return self.cleaned_data['recaptcha_response_field']
130

    
131
    def clean_recaptcha_challenge_field(self):
132
        if 'recaptcha_response_field' in self.cleaned_data:
133
            self.validate_captcha()
134
        return self.cleaned_data['recaptcha_challenge_field']
135

    
136
    def validate_captcha(self):
137
        rcf = self.cleaned_data['recaptcha_challenge_field']
138
        rrf = self.cleaned_data['recaptcha_response_field']
139
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
140
        if not check.is_valid:
141
            raise forms.ValidationError(_('You have not entered the correct words'))
142

    
143
    def save(self, commit=True):
144
        """
145
        Saves the email, first_name and last_name properties, after the normal
146
        save behavior is complete.
147
        """
148
        user = super(LocalUserCreationForm, self).save(commit=False)
149
        user.renew_token()
150
        if commit:
151
            user.save()
152
            logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, [])
153
        return user
154

    
155
class InvitedLocalUserCreationForm(LocalUserCreationForm):
156
    """
157
    Extends the LocalUserCreationForm: email is readonly.
158
    """
159
    class Meta:
160
        model = AstakosUser
161
        fields = ("email", "first_name", "last_name", "has_signed_terms")
162

    
163
    def __init__(self, *args, **kwargs):
164
        """
165
        Changes the order of fields, and removes the username field.
166
        """
167
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
168

    
169
        #set readonly form fields
170
        ro = ('email', 'username',)
171
        for f in ro:
172
            self.fields[f].widget.attrs['readonly'] = True
173

    
174

    
175
    def save(self, commit=True):
176
        user = super(InvitedLocalUserCreationForm, self).save(commit=False)
177
        level = user.invitation.inviter.level + 1
178
        user.level = level
179
        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
180
        user.email_verified = True
181
        if commit:
182
            user.save()
183
        return user
184

    
185
class ThirdPartyUserCreationForm(forms.ModelForm):
186
    third_party_identifier = forms.CharField(
187
        widget=forms.HiddenInput(),
188
        label=''
189
    )
190
    class Meta:
191
        model = AstakosUser
192
        fields = ("email", "first_name", "last_name", "third_party_identifier", "has_signed_terms")
193

    
194
    def __init__(self, *args, **kwargs):
195
        """
196
        Changes the order of fields, and removes the username field.
197
        """
198
        self.request = kwargs.get('request', None)
199
        if self.request:
200
            kwargs.pop('request')
201
                
202
        latest_terms = get_latest_terms()
203
        if latest_terms:
204
            self._meta.fields.append('has_signed_terms')
205
                
206
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
207
        
208
        if latest_terms:
209
            self.fields.keyOrder.append('has_signed_terms')
210
        
211
        if 'has_signed_terms' in self.fields:
212
            # Overriding field label since we need to apply a link
213
            # to the terms within the label
214
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
215
                    % (reverse('latest_terms'), _("the terms"))
216
            self.fields['has_signed_terms'].label = \
217
                    mark_safe("I agree with %s" % terms_link_html)
218

    
219
    def clean_email(self):
220
        email = self.cleaned_data['email']
221
        if not email:
222
            raise forms.ValidationError(_("This field is required"))
223
        return email
224

    
225
    def clean_has_signed_terms(self):
226
        has_signed_terms = self.cleaned_data['has_signed_terms']
227
        if not has_signed_terms:
228
            raise forms.ValidationError(_('You have to agree with the terms'))
229
        return has_signed_terms
230

    
231
    def save(self, commit=True):
232
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
233
        user.set_unusable_password()
234
        user.renew_token()
235
        user.provider = get_query(self.request).get('provider')
236
        if commit:
237
            user.save()
238
            logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, [])
239
        return user
240

    
241
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
242
    """
243
    Extends the ThirdPartyUserCreationForm: email is readonly.
244
    """
245
    def __init__(self, *args, **kwargs):
246
        """
247
        Changes the order of fields, and removes the username field.
248
        """
249
        super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
250

    
251
        #set readonly form fields
252
        ro = ('email',)
253
        for f in ro:
254
            self.fields[f].widget.attrs['readonly'] = True
255

    
256
    def save(self, commit=True):
257
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
258
        level = user.invitation.inviter.level + 1
259
        user.level = level
260
        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
261
        user.email_verified = True
262
        if commit:
263
            user.save()
264
        return user
265

    
266
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
267
    additional_email = forms.CharField(widget=forms.HiddenInput(), label='', required = False)
268

    
269
    def __init__(self, *args, **kwargs):
270
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
271
        # copy email value to additional_mail in case user will change it
272
        name = 'email'
273
        field = self.fields[name]
274
        self.initial['additional_email'] = self.initial.get(name, field.initial)
275
        self.initial['email'] = None
276
    
277
    def clean_email(self):
278
        email = self.cleaned_data['email']
279
        for user in AstakosUser.objects.filter(email = email):
280
            if user.provider == 'shibboleth':
281
                raise forms.ValidationError(_(
282
                        "This email is already associated with another shibboleth \
283
                        account."
284
                    )
285
                )
286
            else:
287
                raise forms.ValidationError(_("This email is already used"))
288
        super(ShibbolethUserCreationForm, self).clean_email()
289
        return email
290

    
291
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
292
    pass
293

    
294
class LoginForm(AuthenticationForm):
295
    username = forms.EmailField(label=_("Email"))
296
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
297
    recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
298

    
299
    def __init__(self, *args, **kwargs):
300
        was_limited = kwargs.get('was_limited', False)
301
        request = kwargs.get('request', None)
302
        if request:
303
            self.ip = request.META.get('REMOTE_ADDR',
304
                                       request.META.get('HTTP_X_REAL_IP', None))
305

    
306
        t = ('request', 'was_limited')
307
        for elem in t:
308
            if elem in kwargs.keys():
309
                kwargs.pop(elem)
310
        super(LoginForm, self).__init__(*args, **kwargs)
311

    
312
        self.fields.keyOrder = ['username', 'password']
313
        if was_limited and RECAPTCHA_ENABLED:
314
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
315
                                         'recaptcha_response_field',])
316

    
317
    def clean_recaptcha_response_field(self):
318
        if 'recaptcha_challenge_field' in self.cleaned_data:
319
            self.validate_captcha()
320
        return self.cleaned_data['recaptcha_response_field']
321

    
322
    def clean_recaptcha_challenge_field(self):
323
        if 'recaptcha_response_field' in self.cleaned_data:
324
            self.validate_captcha()
325
        return self.cleaned_data['recaptcha_challenge_field']
326

    
327
    def validate_captcha(self):
328
        rcf = self.cleaned_data['recaptcha_challenge_field']
329
        rrf = self.cleaned_data['recaptcha_response_field']
330
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
331
        if not check.is_valid:
332
            raise forms.ValidationError(_('You have not entered the correct words'))
333
    
334
    def clean(self):
335
        super(LoginForm, self).clean()
336
        if self.user_cache and self.user_cache.provider not in ('local', ''):
337
            raise forms.ValidationError(_('Local login is not the current authentication method for this account.'))
338
        return self.cleaned_data
339

    
340
class ProfileForm(forms.ModelForm):
341
    """
342
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
343
    Most of the fields are readonly since the user is not allowed to change them.
344

345
    The class defines a save method which sets ``is_verified`` to True so as the user
346
    during the next login will not to be redirected to profile page.
347
    """
348
    renew = forms.BooleanField(label='Renew token', required=False)
349

    
350
    class Meta:
351
        model = AstakosUser
352
        fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires')
353

    
354
    def __init__(self, *args, **kwargs):
355
        super(ProfileForm, self).__init__(*args, **kwargs)
356
        instance = getattr(self, 'instance', None)
357
        ro_fields = ('email', 'auth_token', 'auth_token_expires')
358
        if instance and instance.id:
359
            for field in ro_fields:
360
                self.fields[field].widget.attrs['readonly'] = True
361

    
362
    def save(self, commit=True):
363
        user = super(ProfileForm, self).save(commit=False)
364
        user.is_verified = True
365
        if self.cleaned_data.get('renew'):
366
            user.renew_token()
367
        if commit:
368
            user.save()
369
        return user
370

    
371
class FeedbackForm(forms.Form):
372
    """
373
    Form for writing feedback.
374
    """
375
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
376
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
377
                                    required=False)
378

    
379
class SendInvitationForm(forms.Form):
380
    """
381
    Form for sending an invitations
382
    """
383

    
384
    email = forms.EmailField(required = True, label = 'Email address')
385
    first_name = forms.EmailField(label = 'First name')
386
    last_name = forms.EmailField(label = 'Last name')
387

    
388
class ExtendedPasswordResetForm(PasswordResetForm):
389
    """
390
    Extends PasswordResetForm by overriding save method:
391
    passes a custom from_email in send_mail.
392

393
    Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
394
    accepts a from_email argument.
395
    """
396
    def clean_email(self):
397
        email = super(ExtendedPasswordResetForm, self).clean_email()
398
        try:
399
            user = AstakosUser.objects.get(email=email, is_active=True)
400
            if not user.has_usable_password():
401
                raise forms.ValidationError(_("This account has not a usable password."))
402
        except AstakosUser.DoesNotExist, e:
403
            raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
404
        return email
405

    
406
    def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
407
             use_https=False, token_generator=default_token_generator, request=None):
408
        """
409
        Generates a one-use only link for resetting password and sends to the user.
410
        """
411
        for user in self.users_cache:
412
            url = reverse('django.contrib.auth.views.password_reset_confirm',
413
                          kwargs={'uidb36':int_to_base36(user.id),
414
                                  'token':token_generator.make_token(user)})
415
            url = urljoin(BASEURL, url)
416
            t = loader.get_template(email_template_name)
417
            c = {
418
                'email': user.email,
419
                'url': url,
420
                'site_name': SITENAME,
421
                'user': user,
422
                'baseurl': BASEURL,
423
                'support': DEFAULT_CONTACT_EMAIL
424
            }
425
            from_email = DEFAULT_FROM_EMAIL
426
            send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
427
                t.render(Context(c)), from_email, [user.email])
428

    
429
class EmailChangeForm(forms.ModelForm):
430
    class Meta:
431
        model = EmailChange
432
        fields = ('new_email_address',)
433

    
434
    def clean_new_email_address(self):
435
        addr = self.cleaned_data['new_email_address']
436
        if AstakosUser.objects.filter(email__iexact=addr):
437
            raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
438
        return addr
439

    
440
    def save(self, email_template_name, request, commit=True):
441
        ec = super(EmailChangeForm, self).save(commit=False)
442
        ec.user = request.user
443
        activation_key = hashlib.sha1(str(random()) + smart_str(ec.new_email_address))
444
        ec.activation_key=activation_key.hexdigest()
445
        if commit:
446
            ec.save()
447
        send_change_email(ec, request, email_template_name=email_template_name)
448

    
449
class SignApprovalTermsForm(forms.ModelForm):
450
    class Meta:
451
        model = AstakosUser
452
        fields = ("has_signed_terms",)
453

    
454
    def __init__(self, *args, **kwargs):
455
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
456

    
457
    def clean_has_signed_terms(self):
458
        has_signed_terms = self.cleaned_data['has_signed_terms']
459
        if not has_signed_terms:
460
            raise forms.ValidationError(_('You have to agree with the terms'))
461
        return has_signed_terms
462

    
463
class InvitationForm(forms.ModelForm):
464
    username = forms.EmailField(label=_("Email"))
465

    
466
    def __init__(self, *args, **kwargs):
467
        super(InvitationForm, self).__init__(*args, **kwargs)
468

    
469
    class Meta:
470
        model = Invitation
471
        fields = ('username', 'realname')
472

    
473
    def clean_username(self):
474
        username = self.cleaned_data['username']
475
        try:
476
            Invitation.objects.get(username = username)
477
            raise forms.ValidationError(_('There is already invitation for this email.'))
478
        except Invitation.DoesNotExist:
479
            pass
480
        return username
481

    
482
class ExtendedPasswordChangeForm(PasswordChangeForm):
483
    """
484
    Extends PasswordChangeForm by enabling user
485
    to optionally renew also the token.
486
    """
487
    if not NEWPASSWD_INVALIDATE_TOKEN:
488
        renew = forms.BooleanField(label='Renew token', required=False,
489
                                   initial=True,
490
                                   help_text='Unsetting this may result in security risk.')
491

    
492
    def __init__(self, user, *args, **kwargs):
493
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
494

    
495
    def save(self, commit=True):
496
        if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
497
            self.user.renew_token()
498
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
499

    
500
class ExtendedSetPasswordForm(SetPasswordForm):
501
    """
502
    Extends SetPasswordForm by enabling user
503
    to optionally renew also the token.
504
    """
505
    if not NEWPASSWD_INVALIDATE_TOKEN:
506
        renew = forms.BooleanField(label='Renew token', required=False,
507
                                   initial=True,
508
                                   help_text='Unsetting this may result in security risk.')
509
    
510
    def __init__(self, user, *args, **kwargs):
511
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
512
    
513
    def save(self, commit=True):
514
        if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
515
            if isinstance(self.user, AstakosUser):
516
                self.user.renew_token()
517
        return super(ExtendedSetPasswordForm, self).save(commit=commit)