Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (21.2 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
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
from django.forms.extras.widgets import SelectDateWidget
50
from django.db.models import Q
51

    
52
from astakos.im.models import *
53
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, \
54
    BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, \
55
    RECAPTCHA_ENABLED, LOGGING_LEVEL
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
        if get_latest_terms():
98
            self.fields.keyOrder.append('has_signed_terms')
99
        if RECAPTCHA_ENABLED:
100
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
101
                                         'recaptcha_response_field',])
102

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

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

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

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

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

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

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

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

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

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

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

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

    
235
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
236
    """
237
    Extends the ThirdPartyUserCreationForm: email is readonly.
238
    """
239
    def __init__(self, *args, **kwargs):
240
        """
241
        Changes the order of fields, and removes the username field.
242
        """
243
        super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
244

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

    
260
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
261
    additional_email = forms.CharField(widget=forms.HiddenInput(), label='', required = False)
262
    
263
    def __init__(self, *args, **kwargs):
264
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
265
        self.fields.keyOrder.append('additional_email')
266
        # copy email value to additional_mail in case user will change it
267
        name = 'email'
268
        field = self.fields[name]
269
        self.initial['additional_email'] = self.initial.get(name, field.initial)
270
    
271
    def clean_email(self):
272
        email = self.cleaned_data['email']
273
        for user in AstakosUser.objects.filter(email = email):
274
            if user.provider == 'shibboleth':
275
                raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
276
            elif not user.is_active:
277
                raise forms.ValidationError(_("This email is already associated with an inactive account. \
278
                                              You need to wait to be activated before being able to switch to a shibboleth account."))
279
        super(ShibbolethUserCreationForm, self).clean_email()
280
        return email
281

    
282
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
283
    pass
284
    
285
class LoginForm(AuthenticationForm):
286
    username = forms.EmailField(label=_("Email"))
287
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
288
    recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
289
    
290
    def __init__(self, *args, **kwargs):
291
        was_limited = kwargs.get('was_limited', False)
292
        request = kwargs.get('request', None)
293
        if request:
294
            self.ip = request.META.get('REMOTE_ADDR',
295
                                       request.META.get('HTTP_X_REAL_IP', None))
296
        
297
        t = ('request', 'was_limited')
298
        for elem in t:
299
            if elem in kwargs.keys():
300
                kwargs.pop(elem)
301
        super(LoginForm, self).__init__(*args, **kwargs)
302
        
303
        self.fields.keyOrder = ['username', 'password']
304
        if was_limited and RECAPTCHA_ENABLED:
305
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
306
                                         'recaptcha_response_field',])
307
    
308
    def clean_recaptcha_response_field(self):
309
        if 'recaptcha_challenge_field' in self.cleaned_data:
310
            self.validate_captcha()
311
        return self.cleaned_data['recaptcha_response_field']
312

    
313
    def clean_recaptcha_challenge_field(self):
314
        if 'recaptcha_response_field' in self.cleaned_data:
315
            self.validate_captcha()
316
        return self.cleaned_data['recaptcha_challenge_field']
317

    
318
    def validate_captcha(self):
319
        rcf = self.cleaned_data['recaptcha_challenge_field']
320
        rrf = self.cleaned_data['recaptcha_response_field']
321
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
322
        if not check.is_valid:
323
            raise forms.ValidationError(_('You have not entered the correct words'))
324
    
325
    def clean(self):
326
        super(LoginForm, self).clean()
327
        if self.user_cache and self.user_cache.provider not in ('local', ''):
328
            raise forms.ValidationError(_('Local login is not the current authentication method for this account.'))
329
        return self.cleaned_data
330

    
331
class ProfileForm(forms.ModelForm):
332
    """
333
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
334
    Most of the fields are readonly since the user is not allowed to change them.
335

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

    
341
    class Meta:
342
        model = AstakosUser
343
        fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires')
344

    
345
    def __init__(self, *args, **kwargs):
346
        super(ProfileForm, self).__init__(*args, **kwargs)
347
        instance = getattr(self, 'instance', None)
348
        ro_fields = ('email', 'auth_token', 'auth_token_expires')
349
        if instance and instance.id:
350
            for field in ro_fields:
351
                self.fields[field].widget.attrs['readonly'] = True
352

    
353
    def save(self, commit=True):
354
        user = super(ProfileForm, self).save(commit=False)
355
        user.is_verified = True
356
        if self.cleaned_data.get('renew'):
357
            user.renew_token()
358
        if commit:
359
            user.save()
360
        return user
361

    
362
class FeedbackForm(forms.Form):
363
    """
364
    Form for writing feedback.
365
    """
366
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
367
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
368
                                    required=False)
369

    
370
class SendInvitationForm(forms.Form):
371
    """
372
    Form for sending an invitations
373
    """
374

    
375
    email = forms.EmailField(required = True, label = 'Email address')
376
    first_name = forms.EmailField(label = 'First name')
377
    last_name = forms.EmailField(label = 'Last name')
378

    
379
class ExtendedPasswordResetForm(PasswordResetForm):
380
    """
381
    Extends PasswordResetForm by overriding save method:
382
    passes a custom from_email in send_mail.
383

384
    Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
385
    accepts a from_email argument.
386
    """
387
    def clean_email(self):
388
        email = super(ExtendedPasswordResetForm, self).clean_email()
389
        try:
390
            user = AstakosUser.objects.get(email=email, is_active=True)
391
            if not user.has_usable_password():
392
                raise forms.ValidationError(_("This account has not a usable password."))
393
        except AstakosUser.DoesNotExist, e:
394
            raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
395
        return email
396
    
397
    def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
398
             use_https=False, token_generator=default_token_generator, request=None):
399
        """
400
        Generates a one-use only link for resetting password and sends to the user.
401
        """
402
        for user in self.users_cache:
403
            url = reverse('django.contrib.auth.views.password_reset_confirm',
404
                          kwargs={'uidb36':int_to_base36(user.id),
405
                                  'token':token_generator.make_token(user)})
406
            url = urljoin(BASEURL, url)
407
            t = loader.get_template(email_template_name)
408
            c = {
409
                'email': user.email,
410
                'url': url,
411
                'site_name': SITENAME,
412
                'user': user,
413
                'baseurl': BASEURL,
414
                'support': DEFAULT_CONTACT_EMAIL
415
            }
416
            from_email = DEFAULT_FROM_EMAIL
417
            send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
418
                t.render(Context(c)), from_email, [user.email])
419

    
420
class EmailChangeForm(forms.ModelForm):
421
    class Meta:
422
        model = EmailChange
423
        fields = ('new_email_address',)
424
            
425
    def clean_new_email_address(self):
426
        addr = self.cleaned_data['new_email_address']
427
        if AstakosUser.objects.filter(email__iexact=addr):
428
            raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
429
        return addr
430
    
431
    def save(self, email_template_name, request, commit=True):
432
        ec = super(EmailChangeForm, self).save(commit=False)
433
        ec.user = request.user
434
        activation_key = hashlib.sha1(str(random()) + smart_str(ec.new_email_address))
435
        ec.activation_key=activation_key.hexdigest()
436
        if commit:
437
            ec.save()
438
        send_change_email(ec, request, email_template_name=email_template_name)
439

    
440
class SignApprovalTermsForm(forms.ModelForm):
441
    class Meta:
442
        model = AstakosUser
443
        fields = ("has_signed_terms",)
444

    
445
    def __init__(self, *args, **kwargs):
446
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
447

    
448
    def clean_has_signed_terms(self):
449
        has_signed_terms = self.cleaned_data['has_signed_terms']
450
        if not has_signed_terms:
451
            raise forms.ValidationError(_('You have to agree with the terms'))
452
        return has_signed_terms
453

    
454
class InvitationForm(forms.ModelForm):
455
    username = forms.EmailField(label=_("Email"))
456
    
457
    def __init__(self, *args, **kwargs):
458
        super(InvitationForm, self).__init__(*args, **kwargs)
459
    
460
    class Meta:
461
        model = Invitation
462
        fields = ('username', 'realname')
463
    
464
    def clean_username(self):
465
        username = self.cleaned_data['username']
466
        try:
467
            Invitation.objects.get(username = username)
468
            raise forms.ValidationError(_('There is already invitation for this email.'))
469
        except Invitation.DoesNotExist:
470
            pass
471
        return username
472

    
473
class ExtendedPasswordChangeForm(PasswordChangeForm):
474
    """
475
    Extends PasswordChangeForm by enabling user
476
    to optionally renew also the token.
477
    """
478
    renew = forms.BooleanField(label='Renew token', required=False)
479
    
480
    def __init__(self, user, *args, **kwargs):
481
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
482
    
483
    def save(self, commit=True):
484
        user = super(ExtendedPasswordChangeForm, self).save(commit=False)
485
        if self.cleaned_data.get('renew'):
486
            user.renew_token()
487
        if commit:
488
            user.save()
489
        return user
490

    
491
def get_astakos_group_creation_form(request):
492
    class AstakosGroupCreationForm(forms.ModelForm):
493
        issue_date = forms.DateField(widget=SelectDateWidget())
494
        expiration_date = forms.DateField(widget=SelectDateWidget())
495
        kind = forms.ModelChoiceField(queryset=GroupKind.objects.all(), empty_label=None)
496
        
497
        class Meta:
498
            model = AstakosGroup
499
        
500
        def __init__(self, *args, **kwargs):
501
            super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
502
            self.fields.keyOrder = ['kind', 'name', 'identifier', 'desc', 'issue_date',
503
                                    'expiration_date', 'estimated_participants',
504
                                    'moderatation_enabled']
505
        
506
        def save(self, commit=True):
507
            g = super(AstakosGroupCreationForm, self).save(commit=False)
508
            if commit: 
509
                g.save()
510
                g.owner = [request.user]
511
                g.approve_member(request.user)
512
            return g
513
    
514
    return AstakosGroupCreationForm
515

    
516
def get_astakos_group_policy_creation_form(group):
517
    class AstakosGroupPolicyCreationForm(forms.ModelForm):
518
        choices = Resource.objects.filter(~Q(astakosgroup=group))
519
        resource = forms.ModelChoiceField(queryset=choices, empty_label=None)
520
        
521
        class Meta:
522
            model = AstakosGroupQuota
523
        
524
        def __init__(self, *args, **kwargs):
525
            if not args:
526
                args = ({'group':group},)
527
            super(AstakosGroupPolicyCreationForm, self).__init__(*args, **kwargs)
528
            self.fields['group'].widget.attrs['disabled'] = True
529
    
530
    return AstakosGroupPolicyCreationForm