Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 2b1a5f5d

History | View | Annotate | Download (21.6 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
from django.conf import settings
49

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

    
59
from astakos.im.widgets import DummyWidget, RecaptchaWidget
60
from astakos.im.functions import send_change_email
61

    
62
from astakos.im.util import reserved_email, get_query
63

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

    
69
logger = logging.getLogger(__name__)
70

    
71

    
72
class LocalUserCreationForm(UserCreationForm):
73
    """
74
    Extends the built in UserCreationForm in several ways:
75

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

    
84
    class Meta:
85
        model = AstakosUser
86
        fields = ("email", "first_name", "last_name",
87
                  "has_signed_terms", "has_signed_terms")
88

    
89
    def __init__(self, *args, **kwargs):
90
        """
91
        Changes the order of fields, and removes the username field.
92
        """
93
        request = kwargs.get('request', None)
94
        if request:
95
            kwargs.pop('request')
96
            self.ip = request.META.get('REMOTE_ADDR',
97
                                       request.META.get('HTTP_X_REAL_IP', None))
98

    
99
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
100
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
101
                                'password1', 'password2']
102

    
103
        if RECAPTCHA_ENABLED:
104
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
105
                                         'recaptcha_response_field', ])
106
        if get_latest_terms():
107
            self.fields.keyOrder.append('has_signed_terms')
108

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

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

    
125
    def clean_has_signed_terms(self):
126
        has_signed_terms = self.cleaned_data['has_signed_terms']
127
        if not has_signed_terms:
128
            raise forms.ValidationError(_('You have to agree with the terms'))
129
        return has_signed_terms
130

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

    
136
    def clean_recaptcha_challenge_field(self):
137
        if 'recaptcha_response_field' in self.cleaned_data:
138
            self.validate_captcha()
139
        return self.cleaned_data['recaptcha_challenge_field']
140

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

    
149
    def save(self, commit=True):
150
        """
151
        Saves the email, first_name and last_name properties, after the normal
152
        save behavior is complete.
153
        """
154
        user = super(LocalUserCreationForm, self).save(commit=False)
155
        user.renew_token()
156
        if commit:
157
            user.save()
158
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
159
        return user
160

    
161

    
162
class InvitedLocalUserCreationForm(LocalUserCreationForm):
163
    """
164
    Extends the LocalUserCreationForm: email is readonly.
165
    """
166
    class Meta:
167
        model = AstakosUser
168
        fields = ("email", "first_name", "last_name", "has_signed_terms")
169

    
170
    def __init__(self, *args, **kwargs):
171
        """
172
        Changes the order of fields, and removes the username field.
173
        """
174
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
175

    
176
        #set readonly form fields
177
        ro = ('email', 'username',)
178
        for f in ro:
179
            self.fields[f].widget.attrs['readonly'] = True
180
    
181
    def save(self, commit=True):
182
        user = super(InvitedLocalUserCreationForm, self).save(commit=False)
183
        level = user.invitation.inviter.level + 1
184
        user.level = level
185
        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
186
        user.email_verified = True
187
        if commit:
188
            user.save()
189
        return user
190

    
191

    
192
class ThirdPartyUserCreationForm(forms.ModelForm):
193
    class Meta:
194
        model = AstakosUser
195
        fields = ("email", "first_name", "last_name",
196
                  "third_party_identifier", "has_signed_terms")
197
    
198
    def __init__(self, *args, **kwargs):
199
        """
200
        Changes the order of fields, and removes the username field.
201
        """
202
        self.request = kwargs.get('request', None)
203
        if self.request:
204
            kwargs.pop('request')
205
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
206
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
207
                                'third_party_identifier']
208
        if get_latest_terms():
209
            self.fields.keyOrder.append('has_signed_terms')
210
        #set readonly form fields
211
        ro = ["third_party_identifier"]
212
        for f in ro:
213
            self.fields[f].widget.attrs['readonly'] = True
214

    
215
        if 'has_signed_terms' in self.fields:
216
            # Overriding field label since we need to apply a link
217
            # to the terms within the label
218
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
219
                % (reverse('latest_terms'), _("the terms"))
220
            self.fields['has_signed_terms'].label = \
221
                mark_safe("I agree with %s" % terms_link_html)
222
    
223
    def clean_email(self):
224
        email = self.cleaned_data['email']
225
        if not email:
226
            raise forms.ValidationError(_("This field is required"))
227
        return email
228

    
229
    def clean_has_signed_terms(self):
230
        has_signed_terms = self.cleaned_data['has_signed_terms']
231
        if not has_signed_terms:
232
            raise forms.ValidationError(_('You have to agree with the terms'))
233
        return has_signed_terms
234

    
235
    def save(self, commit=True):
236
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
237
        user.set_unusable_password()
238
        user.renew_token()
239
        user.provider = get_query(self.request).get('provider')
240
        if commit:
241
            user.save()
242
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
243
        return user
244

    
245

    
246
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
247
    """
248
    Extends the ThirdPartyUserCreationForm: email is readonly.
249
    """
250
    def __init__(self, *args, **kwargs):
251
        """
252
        Changes the order of fields, and removes the username field.
253
        """
254
        super(
255
            InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
256

    
257
        #set readonly form fields
258
        ro = ('email',)
259
        for f in ro:
260
            self.fields[f].widget.attrs['readonly'] = True
261

    
262
    def save(self, commit=True):
263
        user = super(
264
            InvitedThirdPartyUserCreationForm, self).save(commit=False)
265
        level = user.invitation.inviter.level + 1
266
        user.level = level
267
        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
268
        user.email_verified = True
269
        if commit:
270
            user.save()
271
        return user
272

    
273

    
274
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
275
    additional_email = forms.CharField(
276
        widget=forms.HiddenInput(), label='', required=False)
277
    
278
    def __init__(self, *args, **kwargs):
279
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
280
        self.fields.keyOrder.append('additional_email')
281
        # copy email value to additional_mail in case user will change it
282
        name = 'email'
283
        field = self.fields[name]
284
        self.initial['additional_email'] = self.initial.get(name,
285
                                                            field.initial)
286
    
287
    def clean_email(self):
288
        email = self.cleaned_data['email']
289
        for user in AstakosUser.objects.filter(email=email):
290
            if user.provider == 'shibboleth':
291
                raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
292
            elif not user.is_active:
293
                raise forms.ValidationError(_("This email is already associated with an inactive account. \
294
                                              You need to wait to be activated before being able to switch to a shibboleth account."))
295
        super(ShibbolethUserCreationForm, self).clean_email()
296
        return email
297

    
298

    
299
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
300
    pass
301

    
302

    
303
class LoginForm(AuthenticationForm):
304
    username = forms.EmailField(label=_("Email"))
305
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
306
    recaptcha_response_field = forms.CharField(
307
        widget=RecaptchaWidget, label='')
308
    
309
    def __init__(self, *args, **kwargs):
310
        was_limited = kwargs.get('was_limited', False)
311
        request = kwargs.get('request', None)
312
        if request:
313
            self.ip = request.META.get('REMOTE_ADDR',
314
                                       request.META.get('HTTP_X_REAL_IP', None))
315

    
316
        t = ('request', 'was_limited')
317
        for elem in t:
318
            if elem in kwargs.keys():
319
                kwargs.pop(elem)
320
        super(LoginForm, self).__init__(*args, **kwargs)
321

    
322
        self.fields.keyOrder = ['username', 'password']
323
        if was_limited and RECAPTCHA_ENABLED:
324
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
325
                                         'recaptcha_response_field', ])
326
    
327
    def clean_recaptcha_response_field(self):
328
        if 'recaptcha_challenge_field' in self.cleaned_data:
329
            self.validate_captcha()
330
        return self.cleaned_data['recaptcha_response_field']
331

    
332
    def clean_recaptcha_challenge_field(self):
333
        if 'recaptcha_response_field' in self.cleaned_data:
334
            self.validate_captcha()
335
        return self.cleaned_data['recaptcha_challenge_field']
336

    
337
    def validate_captcha(self):
338
        rcf = self.cleaned_data['recaptcha_challenge_field']
339
        rrf = self.cleaned_data['recaptcha_response_field']
340
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
341
        if not check.is_valid:
342
            raise forms.ValidationError(
343
                _('You have not entered the correct words'))
344
    
345
    def clean(self):
346
        super(LoginForm, self).clean()
347
        if self.user_cache and self.user_cache.provider not in ('local', ''):
348
            raise forms.ValidationError(_('Local login is not the current authentication method for this account.'))
349
        return self.cleaned_data
350

    
351

    
352
class ProfileForm(forms.ModelForm):
353
    """
354
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
355
    Most of the fields are readonly since the user is not allowed to change them.
356

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

    
362
    class Meta:
363
        model = AstakosUser
364
        fields = ('email', 'first_name', 'last_name', 'auth_token',
365
                  'auth_token_expires')
366

    
367
    def __init__(self, *args, **kwargs):
368
        super(ProfileForm, self).__init__(*args, **kwargs)
369
        instance = getattr(self, 'instance', None)
370
        ro_fields = ('email', 'auth_token', 'auth_token_expires')
371
        if instance and instance.id:
372
            for field in ro_fields:
373
                self.fields[field].widget.attrs['readonly'] = True
374

    
375
    def save(self, commit=True):
376
        user = super(ProfileForm, self).save(commit=False)
377
        user.is_verified = True
378
        if self.cleaned_data.get('renew'):
379
            user.renew_token()
380
        if commit:
381
            user.save()
382
        return user
383

    
384

    
385
class FeedbackForm(forms.Form):
386
    """
387
    Form for writing feedback.
388
    """
389
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
390
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
391
                                    required=False)
392

    
393

    
394
class SendInvitationForm(forms.Form):
395
    """
396
    Form for sending an invitations
397
    """
398

    
399
    email = forms.EmailField(required=True, label='Email address')
400
    first_name = forms.EmailField(label='First name')
401
    last_name = forms.EmailField(label='Last name')
402

    
403

    
404
class ExtendedPasswordResetForm(PasswordResetForm):
405
    """
406
    Extends PasswordResetForm by overriding save method:
407
    passes a custom from_email in send_mail.
408

409
    Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
410
    accepts a from_email argument.
411
    """
412
    def clean_email(self):
413
        email = super(ExtendedPasswordResetForm, self).clean_email()
414
        try:
415
            user = AstakosUser.objects.get(email=email, is_active=True)
416
            if not user.has_usable_password():
417
                raise forms.ValidationError(
418
                    _("This account has not a usable password."))
419
        except AstakosUser.DoesNotExist:
420
            raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
421
        return email
422

    
423
    def save(
424
        self, domain_override=None, email_template_name='registration/password_reset_email.html',
425
            use_https=False, token_generator=default_token_generator, request=None):
426
        """
427
        Generates a one-use only link for resetting password and sends to the user.
428
        """
429
        for user in self.users_cache:
430
            url = reverse('django.contrib.auth.views.password_reset_confirm',
431
                          kwargs={'uidb36': int_to_base36(user.id),
432
                                  'token': token_generator.make_token(user)
433
                                  }
434
                          )
435
            url = urljoin(BASEURL, url)
436
            t = loader.get_template(email_template_name)
437
            c = {
438
                'email': user.email,
439
                'url': url,
440
                'site_name': SITENAME,
441
                'user': user,
442
                'baseurl': BASEURL,
443
                'support': DEFAULT_CONTACT_EMAIL
444
            }
445
            from_email = settings.SERVER_EMAIL
446
            send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
447
                      t.render(Context(c)), from_email, [user.email])
448

    
449

    
450
class EmailChangeForm(forms.ModelForm):
451
    class Meta:
452
        model = EmailChange
453
        fields = ('new_email_address',)
454

    
455
    def clean_new_email_address(self):
456
        addr = self.cleaned_data['new_email_address']
457
        if AstakosUser.objects.filter(email__iexact=addr):
458
            raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
459
        return addr
460

    
461
    def save(self, email_template_name, request, commit=True):
462
        ec = super(EmailChangeForm, self).save(commit=False)
463
        ec.user = request.user
464
        activation_key = hashlib.sha1(
465
            str(random()) + smart_str(ec.new_email_address))
466
        ec.activation_key = activation_key.hexdigest()
467
        if commit:
468
            ec.save()
469
        send_change_email(ec, request, email_template_name=email_template_name)
470

    
471

    
472
class SignApprovalTermsForm(forms.ModelForm):
473
    class Meta:
474
        model = AstakosUser
475
        fields = ("has_signed_terms",)
476

    
477
    def __init__(self, *args, **kwargs):
478
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
479

    
480
    def clean_has_signed_terms(self):
481
        has_signed_terms = self.cleaned_data['has_signed_terms']
482
        if not has_signed_terms:
483
            raise forms.ValidationError(_('You have to agree with the terms'))
484
        return has_signed_terms
485

    
486

    
487
class InvitationForm(forms.ModelForm):
488
    username = forms.EmailField(label=_("Email"))
489

    
490
    def __init__(self, *args, **kwargs):
491
        super(InvitationForm, self).__init__(*args, **kwargs)
492

    
493
    class Meta:
494
        model = Invitation
495
        fields = ('username', 'realname')
496

    
497
    def clean_username(self):
498
        username = self.cleaned_data['username']
499
        try:
500
            Invitation.objects.get(username=username)
501
            raise forms.ValidationError(
502
                _('There is already invitation for this email.'))
503
        except Invitation.DoesNotExist:
504
            pass
505
        return username
506

    
507

    
508
class ExtendedPasswordChangeForm(PasswordChangeForm):
509
    """
510
    Extends PasswordChangeForm by enabling user
511
    to optionally renew also the token.
512
    """
513
    renew = forms.BooleanField(label='Renew token', required=False)
514

    
515
    def __init__(self, user, *args, **kwargs):
516
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
517

    
518
    def save(self, commit=True):
519
        user = super(ExtendedPasswordChangeForm, self).save(commit=False)
520
        if self.cleaned_data.get('renew'):
521
            user.renew_token()
522
        if commit:
523
            user.save()
524
        return user
525

    
526

    
527
class AstakosGroupCreationForm(forms.ModelForm):
528
#     issue_date = forms.DateField(widget=SelectDateWidget())
529
#     expiration_date = forms.DateField(widget=SelectDateWidget())
530
    kind = forms.ModelChoiceField(
531
        queryset=GroupKind.objects.all(),
532
        label="",
533
        widget=forms.HiddenInput()
534
    )
535
    name = forms.URLField()
536
    moderation_enabled = forms.BooleanField(
537
        help_text="Check if you want to approve members participation manually",
538
        required=False   
539
    )
540
    
541
    class Meta:
542
        model = AstakosGroup
543

    
544
    def __init__(self, *args, **kwargs):
545
        try:
546
            resources = kwargs.pop('resources')
547
        except KeyError:
548
            resources = {}
549
        super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
550
        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc', 'issue_date',
551
                                'expiration_date', 'estimated_participants',
552
                                'moderation_enabled']
553
        for id, r in resources.iteritems():
554
            self.fields['resource_%s' % id] = forms.IntegerField(
555
                label=r,
556
                required=False,
557
                help_text=_('Leave it blank for no additional quota.')
558
            )
559

    
560
    def resources(self):
561
        for name, value in self.cleaned_data.items():
562
            prefix, delimiter, suffix = name.partition('resource_')
563
            if suffix:
564
                # yield only those having a value
565
                if not value:
566
                    continue
567
                yield (suffix, value)
568

    
569
class AstakosGroupUpdateForm(forms.ModelForm):
570
    class Meta:
571
        model = AstakosGroup
572
        fields = ('homepage', 'desc')
573

    
574
class AstakosGroupSearchForm(forms.Form):
575
    q = forms.CharField(max_length=200, label='Search group')