Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 0f4fa26d

History | View | Annotate | Download (21.7 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
from django.db.models.query import EmptyQuerySet
52

    
53
from astakos.im.models import *
54
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, \
55
    BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, \
56
    RECAPTCHA_ENABLED, LOGGING_LEVEL
57
from astakos.im.widgets import DummyWidget, RecaptchaWidget
58
from astakos.im.functions import send_change_email
59

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

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

    
68
logger = logging.getLogger(__name__)
69

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
447
    def __init__(self, *args, **kwargs):
448
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
449

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

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

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

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

    
531
class AstakosGroupSearchForm(forms.Form):
532
    q = forms.CharField(max_length=200, label='')
533

    
534
class MembershipCreationForm(forms.ModelForm):
535
    # TODO check not to hit the db
536
    group = forms.ModelChoiceField(
537
        queryset=AstakosGroup.objects.all(),
538
        widget=forms.HiddenInput()
539
    )
540
    person = forms.ModelChoiceField(
541
        queryset=AstakosUser.objects.all(),
542
        widget=forms.HiddenInput()
543
    )
544
    date_requested = forms.DateField(
545
        widget=forms.HiddenInput(),
546
        input_formats="%d/%m/%Y"
547
    )
548
    
549
    class Meta:
550
        model = Membership
551
        exclude = ('date_joined',)
552
    
553
    def __init__(self, *args, **kwargs):
554
        super(MembershipCreationForm, self).__init__(*args, **kwargs)