Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (20.8 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

    
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
39
)
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

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

    
59
from astakos.im.util import reserved_email, get_query
60

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

    
66
logger = logging.getLogger(__name__)
67

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

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

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

    
83
    def __init__(self, *args, **kwargs):
84
        """
85
        Changes the order of fields, and removes the username field.
86
        """
87
        request = kwargs.get('request', None)
88
        if request:
89
            kwargs.pop('request')
90
            self.ip = request.META.get('REMOTE_ADDR',
91
                                       request.META.get('HTTP_X_REAL_IP', None))
92
        
93
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
94
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
95
                                'password1', 'password2']
96

    
97
        if RECAPTCHA_ENABLED:
98
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
99
                                         'recaptcha_response_field',])
100
        if get_latest_terms():
101
            self.fields.keyOrder.append('has_signed_terms')
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:
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
class AstakosGroupCreationForm(forms.ModelForm):
492
#     issue_date = forms.DateField(widget=SelectDateWidget())
493
#     expiration_date = forms.DateField(widget=SelectDateWidget())
494
    kind = forms.ModelChoiceField(
495
        queryset=GroupKind.objects.all(),
496
        label="",
497
        widget=forms.HiddenInput()
498
    )
499
    name = forms.URLField()
500
    
501
    class Meta:
502
        model = AstakosGroup
503
    
504
    def __init__(self, *args, **kwargs):
505
        try:
506
            resources = kwargs.pop('resources')
507
        except KeyError:
508
            resources = {}
509
        super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
510
        self.fields.keyOrder = ['kind', 'name', 'desc', 'issue_date',
511
                                'expiration_date', 'estimated_participants',
512
                                'moderation_enabled']
513
        for id, r in resources.iteritems():
514
            self.fields['resource_%s' % id] = forms.IntegerField(
515
                label=r,
516
                required=False,
517
                help_text=_('Leave it blank for no additional quota.')
518
            )
519
        
520
    def resources(self):
521
        for name, value in self.cleaned_data.items():
522
            prefix, delimiter, suffix = name.partition('resource_')
523
            if suffix:
524
                # yield only those having a value
525
                if not value:
526
                    continue
527
                yield (suffix, value)
528

    
529
class AstakosGroupSearchForm(forms.Form):
530
    q = forms.CharField(max_length=200, label='')