forms.py 18 KB
Newer Older
Antony Chazapis's avatar
Antony Chazapis committed
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
2
#
3
4
5
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
6
#
7
8
9
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
10
#
11
12
13
14
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
15
#
16
17
18
19
20
21
22
23
24
25
26
27
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
28
#
29
30
31
32
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
33
from urlparse import urljoin
34
from datetime import datetime
35
36
37

from django import forms
from django.utils.translation import ugettext as _
38
39
40
41
42
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, PasswordResetForm
from django.core.mail import send_mail
from django.contrib.auth.tokens import default_token_generator
from django.template import Context, loader
from django.utils.http import int_to_base36
43
from django.core.urlresolvers import reverse
44
from django.utils.functional import lazy
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
45
from django.utils.safestring import mark_safe
46
from django.contrib import messages
47
from django.utils.encoding import smart_str
48

49
from astakos.im.models import AstakosUser, Invitation, get_latest_terms, EmailChange
50
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, RECAPTCHA_ENABLED
51
from astakos.im.widgets import DummyWidget, RecaptchaWidget
52
from astakos.im.functions import send_change_email
53
54

# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
55
from astakos.im.util import reverse_lazy, reserved_email, get_query
56

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
57
import logging
58
import hashlib
59
import recaptcha.client.captcha as captcha
60
from random import random
61

62
63
logger = logging.getLogger(__name__)

64
class LocalUserCreationForm(UserCreationForm):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
65
66
    """
    Extends the built in UserCreationForm in several ways:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
67

68
    * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
69
    * The username field isn't visible and it is assigned a generated id.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
70
    * User created is not active.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
71
    """
72
73
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
    recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
74

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
75
76
    class Meta:
        model = AstakosUser
77
        fields = ("email", "first_name", "last_name", "has_signed_terms", "has_signed_terms")
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
78

79
80
    def __init__(self, *args, **kwargs):
        """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
81
        Changes the order of fields, and removes the username field.
82
        """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
83
84
85
86
87
88
        request = kwargs.get('request', None)
        if request:
            kwargs.pop('request')
            self.ip = request.META.get('REMOTE_ADDR',
                                       request.META.get('HTTP_X_REAL_IP', None))
        
89
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
90
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
91
92
93
                                'password1', 'password2']
        if get_latest_terms():
            self.fields.keyOrder.append('has_signed_terms')
94
95
96
        if RECAPTCHA_ENABLED:
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
                                         'recaptcha_response_field',])
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
97
98
99
100
101
102
103
104
105

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

106
107
    def clean_email(self):
        email = self.cleaned_data['email']
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
108
109
        if not email:
            raise forms.ValidationError(_("This field is required"))
110
        if reserved_email(email):
111
            raise forms.ValidationError(_("This email is already used"))
112
        return email
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
113

114
115
116
117
118
    def clean_has_signed_terms(self):
        has_signed_terms = self.cleaned_data['has_signed_terms']
        if not has_signed_terms:
            raise forms.ValidationError(_('You have to agree with the terms'))
        return has_signed_terms
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
119

120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
    def clean_recaptcha_response_field(self):
        if 'recaptcha_challenge_field' in self.cleaned_data:
            self.validate_captcha()
        return self.cleaned_data['recaptcha_response_field']

    def clean_recaptcha_challenge_field(self):
        if 'recaptcha_response_field' in self.cleaned_data:
            self.validate_captcha()
        return self.cleaned_data['recaptcha_challenge_field']

    def validate_captcha(self):
        rcf = self.cleaned_data['recaptcha_challenge_field']
        rrf = self.cleaned_data['recaptcha_response_field']
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
        if not check.is_valid:
            raise forms.ValidationError(_('You have not entered the correct words'))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
136

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
137
    def save(self, commit=True):
138
        """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
139
140
141
        Saves the email, first_name and last_name properties, after the normal
        save behavior is complete.
        """
142
        user = super(LocalUserCreationForm, self).save(commit=False)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
143
        user.renew_token()
144
145
        if commit:
            user.save()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
146
            logger.info('Created user %s', user)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
147
        return user
148

149
class InvitedLocalUserCreationForm(LocalUserCreationForm):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
150
    """
151
    Extends the LocalUserCreationForm: email is readonly.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
152
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
153
154
    class Meta:
        model = AstakosUser
155
        fields = ("email", "first_name", "last_name", "has_signed_terms")
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
156

157
158
    def __init__(self, *args, **kwargs):
        """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
159
        Changes the order of fields, and removes the username field.
160
        """
161
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
162

163
        #set readonly form fields
164
        ro = ('email', 'username',)
165
166
167
        for f in ro:
            self.fields[f].widget.attrs['readonly'] = True
        
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
168

169
    def save(self, commit=True):
170
        user = super(InvitedLocalUserCreationForm, self).save(commit=False)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
171
172
        level = user.invitation.inviter.level + 1
        user.level = level
173
174
        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
        user.email_verified = True
175
176
177
        if commit:
            user.save()
        return user
178

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
179
class ThirdPartyUserCreationForm(forms.ModelForm):
180
181
    class Meta:
        model = AstakosUser
182
        fields = ("email", "first_name", "last_name", "third_party_identifier", "has_signed_terms")
183
184
185
186
187
    
    def __init__(self, *args, **kwargs):
        """
        Changes the order of fields, and removes the username field.
        """
188
189
190
        self.request = kwargs.get('request', None)
        if self.request:
            kwargs.pop('request')
191
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
192
        self.fields.keyOrder = ['email', 'first_name', 'last_name', 'third_party_identifier']
193
194
        if get_latest_terms():
            self.fields.keyOrder.append('has_signed_terms')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
195
        #set readonly form fields
196
        ro = ["third_party_identifier", "first_name", "last_name"]
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
        for f in ro:
            self.fields[f].widget.attrs['readonly'] = True
        
        if 'has_signed_terms' in self.fields:
            # Overriding field label since we need to apply a link
            # to the terms within the label
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
                    % (reverse('latest_terms'), _("the terms"))
            self.fields['has_signed_terms'].label = \
                    mark_safe("I agree with %s" % terms_link_html)
    
    def clean_email(self):
        email = self.cleaned_data['email']
        if not email:
            raise forms.ValidationError(_("This field is required"))
212
        return email
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
213
214
215
216
217
218
    
    def clean_has_signed_terms(self):
        has_signed_terms = self.cleaned_data['has_signed_terms']
        if not has_signed_terms:
            raise forms.ValidationError(_('You have to agree with the terms'))
        return has_signed_terms
219
220
221
222
    
    def save(self, commit=True):
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
        user.set_unusable_password()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
223
        user.renew_token()
224
        user.provider = get_query(self.request).get('provider')
225
226
        if commit:
            user.save()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
227
            logger.info('Created user %s', user)
228
229
230
        return user

class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
231
    """
232
    Extends the ThirdPartyUserCreationForm: email is readonly.
233
    """
234
    def __init__(self, *args, **kwargs):
235
236
237
        """
        Changes the order of fields, and removes the username field.
        """
238
        super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
239

240
        #set readonly form fields
241
        ro = ('email',)
242
243
244
245
246
247
248
249
250
251
252
253
        for f in ro:
            self.fields[f].widget.attrs['readonly'] = True
    
    def save(self, commit=True):
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
        level = user.invitation.inviter.level + 1
        user.level = level
        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
        user.email_verified = True
        if commit:
            user.save()
        return user
254

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
255
256
257
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
    def clean_email(self):
        email = self.cleaned_data['email']
258
259
        for user in AstakosUser.objects.filter(email = email):
            if user.provider == 'shibboleth':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
260
                raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
261
262
263
            elif not user.is_active:
                raise forms.ValidationError(_("This email is already associated with an inactive account. \
                                              You need to wait to be activated before being able to switch to a shibboleth account."))
264
        super(ShibbolethUserCreationForm, self).clean_email()
265
        return email
266

267
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
268
    pass
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
269
    
270
271
class LoginForm(AuthenticationForm):
    username = forms.EmailField(label=_("Email"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
    recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
    
    def __init__(self, *args, **kwargs):
        was_limited = kwargs.get('was_limited', False)
        request = kwargs.get('request', None)
        if request:
            self.ip = request.META.get('REMOTE_ADDR',
                                       request.META.get('HTTP_X_REAL_IP', None))
        
        t = ('request', 'was_limited')
        for elem in t:
            if elem in kwargs.keys():
                kwargs.pop(elem)
        super(LoginForm, self).__init__(*args, **kwargs)
        
        self.fields.keyOrder = ['username', 'password']
        if was_limited and RECAPTCHA_ENABLED:
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
                                         'recaptcha_response_field',])
    
    def clean_recaptcha_response_field(self):
        if 'recaptcha_challenge_field' in self.cleaned_data:
            self.validate_captcha()
        return self.cleaned_data['recaptcha_response_field']

    def clean_recaptcha_challenge_field(self):
        if 'recaptcha_response_field' in self.cleaned_data:
            self.validate_captcha()
        return self.cleaned_data['recaptcha_challenge_field']

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
310
311
312
313
class ProfileForm(forms.ModelForm):
    """
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
    Most of the fields are readonly since the user is not allowed to change them.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
314

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
315
316
317
    The class defines a save method which sets ``is_verified`` to True so as the user
    during the next login will not to be redirected to profile page.
    """
318
    renew = forms.BooleanField(label='Renew token', required=False)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
319

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
320
321
    class Meta:
        model = AstakosUser
322
        fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
323

324
    def __init__(self, *args, **kwargs):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
325
326
        super(ProfileForm, self).__init__(*args, **kwargs)
        instance = getattr(self, 'instance', None)
327
        ro_fields = ('email', 'auth_token', 'auth_token_expires')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
328
329
330
        if instance and instance.id:
            for field in ro_fields:
                self.fields[field].widget.attrs['readonly'] = True
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
331

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
332
333
334
    def save(self, commit=True):
        user = super(ProfileForm, self).save(commit=False)
        user.is_verified = True
335
336
        if self.cleaned_data.get('renew'):
            user.renew_token()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
337
338
339
        if commit:
            user.save()
        return user
340

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
341
342
343
344
class FeedbackForm(forms.Form):
    """
    Form for writing feedback.
    """
345
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
346
347
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
                                    required=False)
348
349
350
351
352

class SendInvitationForm(forms.Form):
    """
    Form for sending an invitations
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
353

354
355
356
    email = forms.EmailField(required = True, label = 'Email address')
    first_name = forms.EmailField(label = 'First name')
    last_name = forms.EmailField(label = 'Last name')
357
358
359
360
361

class ExtendedPasswordResetForm(PasswordResetForm):
    """
    Extends PasswordResetForm by overriding save method:
    passes a custom from_email in send_mail.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
362

363
364
365
    Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
    accepts a from_email argument.
    """
366
367
368
    def clean_email(self):
        email = super(ExtendedPasswordResetForm, self).clean_email()
        try:
369
            user = AstakosUser.objects.get(email=email, is_active=True)
370
371
372
373
374
375
            if not user.has_usable_password():
                raise forms.ValidationError(_("This account has not a usable password."))
        except AstakosUser.DoesNotExist, e:
            raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
        return email
    
376
377
378
379
380
381
    def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
             use_https=False, token_generator=default_token_generator, request=None):
        """
        Generates a one-use only link for resetting password and sends to the user.
        """
        for user in self.users_cache:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
382
383
384
            url = reverse('django.contrib.auth.views.password_reset_confirm',
                          kwargs={'uidb36':int_to_base36(user.id),
                                  'token':token_generator.make_token(user)})
385
            url = urljoin(BASEURL, url)
386
387
388
            t = loader.get_template(email_template_name)
            c = {
                'email': user.email,
389
                'url': url,
390
                'site_name': SITENAME,
391
                'user': user,
392
                'baseurl': BASEURL,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
393
                'support': DEFAULT_CONTACT_EMAIL
394
            }
395
            from_email = DEFAULT_FROM_EMAIL
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
396
            send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
397
                t.render(Context(c)), from_email, [user.email])
398

399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
class EmailChangeForm(forms.ModelForm):
    class Meta:
        model = EmailChange
        fields = ('new_email_address',)
            
    def clean_new_email_address(self):
        addr = self.cleaned_data['new_email_address']
        if AstakosUser.objects.filter(email__iexact=addr):
            raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
        return addr
    
    def save(self, email_template_name, request, commit=True):
        ec = super(EmailChangeForm, self).save(commit=False)
        ec.user = request.user
        activation_key = hashlib.sha1(str(random()) + smart_str(ec.new_email_address))
        ec.activation_key=activation_key.hexdigest()
        if commit:
            ec.save()
        send_change_email(ec, request, email_template_name=email_template_name)

419
420
421
422
class SignApprovalTermsForm(forms.ModelForm):
    class Meta:
        model = AstakosUser
        fields = ("has_signed_terms",)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
423

424
425
    def __init__(self, *args, **kwargs):
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
426

427
428
429
430
431
    def clean_has_signed_terms(self):
        has_signed_terms = self.cleaned_data['has_signed_terms']
        if not has_signed_terms:
            raise forms.ValidationError(_('You have to agree with the terms'))
        return has_signed_terms
432
433
434
435

class InvitationForm(forms.ModelForm):
    username = forms.EmailField(label=_("Email"))
    
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
436
437
438
    def __init__(self, *args, **kwargs):
        super(InvitationForm, self).__init__(*args, **kwargs)
    
439
440
441
442
443
444
445
446
447
448
449
    class Meta:
        model = Invitation
        fields = ('username', 'realname')
    
    def clean_username(self):
        username = self.cleaned_data['username']
        try:
            Invitation.objects.get(username = username)
            raise forms.ValidationError(_('There is already invitation for this email.'))
        except Invitation.DoesNotExist:
            pass
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
450
        return username