Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 23c271b3

History | View | Annotate | Download (16.1 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, PasswordResetForm
39
from django.core.mail import send_mail
40
from django.contrib.auth.tokens import default_token_generator
41
from django.template import Context, loader
42
from django.utils.http import int_to_base36
43
from django.core.urlresolvers import reverse
44
from django.utils.functional import lazy
45
from django.utils.safestring import mark_safe
46

    
47
from astakos.im.models import AstakosUser, Invitation
48
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, RECAPTCHA_ENABLED
49
from astakos.im.widgets import DummyWidget, RecaptchaWidget, ApprovalTermsWidget
50

    
51
# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
52
from astakos.im.util import reverse_lazy, get_latest_terms
53

    
54
import logging
55
import recaptcha.client.captcha as captcha
56

    
57
logger = logging.getLogger(__name__)
58

    
59
class LocalUserCreationForm(UserCreationForm):
60
    """
61
    Extends the built in UserCreationForm in several ways:
62

63
    * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
64
    * The username field isn't visible and it is assigned a generated id.
65
    * User created is not active.
66
    """
67
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
68
    recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
69

    
70
    class Meta:
71
        model = AstakosUser
72
        fields = ("email", "first_name", "last_name", "has_signed_terms")
73
        widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
74

    
75
    def __init__(self, *args, **kwargs):
76
        """
77
        Changes the order of fields, and removes the username field.
78
        """
79
        if 'ip' in kwargs:
80
            self.ip = kwargs['ip']
81
            kwargs.pop('ip')
82
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
83
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
84
                                'password1', 'password2']
85
        if get_latest_terms():
86
            self.fields.keyOrder.append('has_signed_terms')
87
        if RECAPTCHA_ENABLED:
88
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
89
                                         'recaptcha_response_field',])
90

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

    
99
    def clean_email(self):
100
        email = self.cleaned_data['email']
101
        if not email:
102
            raise forms.ValidationError(_("This field is required"))
103
        try:
104
            AstakosUser.objects.get(email = email)
105
            raise forms.ValidationError(_("This email is already used"))
106
        except AstakosUser.DoesNotExist:
107
            return email
108

    
109
    def clean_has_signed_terms(self):
110
        has_signed_terms = self.cleaned_data['has_signed_terms']
111
        if not has_signed_terms:
112
            raise forms.ValidationError(_('You have to agree with the terms'))
113
        return has_signed_terms
114

    
115
    def clean_recaptcha_response_field(self):
116
        if 'recaptcha_challenge_field' in self.cleaned_data:
117
            self.validate_captcha()
118
        return self.cleaned_data['recaptcha_response_field']
119

    
120
    def clean_recaptcha_challenge_field(self):
121
        if 'recaptcha_response_field' in self.cleaned_data:
122
            self.validate_captcha()
123
        return self.cleaned_data['recaptcha_challenge_field']
124

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

    
132
    def save(self, commit=True):
133
        """
134
        Saves the email, first_name and last_name properties, after the normal
135
        save behavior is complete.
136
        """
137
        user = super(LocalUserCreationForm, self).save(commit=False)
138
        user.renew_token()
139
        if commit:
140
            user.save()
141
        logger.info('Created user %s', user)
142
        return user
143

    
144
class InvitedLocalUserCreationForm(LocalUserCreationForm):
145
    """
146
    Extends the LocalUserCreationForm: adds an inviter readonly field.
147
    """
148

    
149
    inviter = forms.CharField(widget=forms.TextInput(), label=_('Inviter Real Name'))
150

    
151
    class Meta:
152
        model = AstakosUser
153
        fields = ("email", "first_name", "last_name", "has_signed_terms")
154
        widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
155

    
156
    def __init__(self, *args, **kwargs):
157
        """
158
        Changes the order of fields, and removes the username field.
159
        """
160
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
161

    
162
        #set readonly form fields
163
        ro = ('inviter', 'email', 'username',)
164
        for f in ro:
165
            self.fields[f].widget.attrs['readonly'] = True
166
        
167

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

    
178
class ThirdPartyUserCreationForm(forms.ModelForm):
179
    class Meta:
180
        model = AstakosUser
181
        fields = ("email", "first_name", "last_name", "third_party_identifier",
182
                  "has_signed_terms", "provider")
183
        widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
184
    
185
    def __init__(self, *args, **kwargs):
186
        """
187
        Changes the order of fields, and removes the username field.
188
        """
189
        if 'ip' in kwargs:
190
            kwargs.pop('ip')
191
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
192
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
193
                                'provider', 'third_party_identifier']
194
        if get_latest_terms():
195
            self.fields.keyOrder.append('has_signed_terms')
196
        #set readonly form fields
197
        ro = ["provider", "third_party_identifier", "first_name", "last_name"]
198
        for f in ro:
199
            self.fields[f].widget.attrs['readonly'] = True
200
        
201
        if 'has_signed_terms' in self.fields:
202
            # Overriding field label since we need to apply a link
203
            # to the terms within the label
204
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
205
                    % (reverse('latest_terms'), _("the terms"))
206
            self.fields['has_signed_terms'].label = \
207
                    mark_safe("I agree with %s" % terms_link_html)
208
    
209
    def clean_email(self):
210
        email = self.cleaned_data['email']
211
        if not email:
212
            raise forms.ValidationError(_("This field is required"))
213
        try:
214
            AstakosUser.objects.get(email = email)
215
            raise forms.ValidationError(_("This email is already used"))
216
        except AstakosUser.DoesNotExist:
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
        if commit:
230
            user.save()
231
        logger.info('Created user %s', user)
232
        return user
233

    
234
#class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
235
#    def __init__(self, *args, **kwargs):
236
#        super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
237
#        #set readonly form fields
238
#        self.fields['email'].widget.attrs['readonly'] = True
239

    
240
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
241
    """
242
    Extends the LocalUserCreationForm: adds an inviter readonly field.
243
    """
244
    inviter = forms.CharField(widget=forms.TextInput(), label=_('Inviter Real Name'))
245
    
246
    def __init__(self, *args, **kwargs):
247
        """
248
        Changes the order of fields, and removes the username field.
249
        """
250
        super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
251

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

    
267
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
268
    def clean_email(self):
269
        email = self.cleaned_data['email']
270
        if not email:
271
            raise forms.ValidationError(_("This field is required"))
272
        try:
273
            user = AstakosUser.objects.get(email = email)
274
            if user.provider == 'local':
275
                self.instance = user
276
                return email
277
            else:
278
                raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
279
        except AstakosUser.DoesNotExist:
280
            return email
281

    
282
class InvitedShibbolethUserCreationForm(InvitedThirdPartyUserCreationForm):
283
    pass
284
    
285
class LoginForm(AuthenticationForm):
286
    username = forms.EmailField(label=_("Email"))
287

    
288
class ProfileForm(forms.ModelForm):
289
    """
290
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
291
    Most of the fields are readonly since the user is not allowed to change them.
292

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

    
298
    class Meta:
299
        model = AstakosUser
300
        fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires', 'groups')
301

    
302
    def __init__(self, *args, **kwargs):
303
        super(ProfileForm, self).__init__(*args, **kwargs)
304
        instance = getattr(self, 'instance', None)
305
        ro_fields = ('auth_token', 'auth_token_expires', 'groups')
306
        if instance and instance.id:
307
            for field in ro_fields:
308
                self.fields[field].widget.attrs['readonly'] = True
309

    
310
    def save(self, commit=True):
311
        user = super(ProfileForm, self).save(commit=False)
312
        user.is_verified = True
313
        if self.cleaned_data.get('renew'):
314
            user.renew_token()
315
        if commit:
316
            user.save()
317
        return user
318

    
319
class FeedbackForm(forms.Form):
320
    """
321
    Form for writing feedback.
322
    """
323
    feedback_msg = forms.CharField(widget=forms.TextInput(), label=u'Message')
324
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
325
                                    required=False)
326

    
327
class SendInvitationForm(forms.Form):
328
    """
329
    Form for sending an invitations
330
    """
331

    
332
    email = forms.EmailField(required = True, label = 'Email address')
333
    first_name = forms.EmailField(label = 'First name')
334
    last_name = forms.EmailField(label = 'Last name')
335

    
336
class ExtendedPasswordResetForm(PasswordResetForm):
337
    """
338
    Extends PasswordResetForm by overriding save method:
339
    passes a custom from_email in send_mail.
340

341
    Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
342
    accepts a from_email argument.
343
    """
344
    def clean_email(self):
345
        email = super(ExtendedPasswordResetForm, self).clean_email()
346
        try:
347
            user = AstakosUser.objects.get(email=email)
348
            if not user.has_usable_password():
349
                raise forms.ValidationError(_("This account has not a usable password."))
350
        except AstakosUser.DoesNotExist, e:
351
            raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
352
        return email
353
    
354
    def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
355
             use_https=False, token_generator=default_token_generator, request=None):
356
        """
357
        Generates a one-use only link for resetting password and sends to the user.
358
        """
359
        for user in self.users_cache:
360
            url = reverse('django.contrib.auth.views.password_reset_confirm',
361
                          kwargs={'uidb36':int_to_base36(user.id),
362
                                  'token':token_generator.make_token(user)})
363
            url = request.build_absolute_uri(url)
364
            t = loader.get_template(email_template_name)
365
            c = {
366
                'email': user.email,
367
                'url': url,
368
                'site_name': SITENAME,
369
                'user': user,
370
                'baseurl': request.build_absolute_uri(),
371
                'support': DEFAULT_CONTACT_EMAIL
372
            }
373
            from_email = DEFAULT_FROM_EMAIL
374
            send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
375
                t.render(Context(c)), from_email, [user.email])
376

    
377
class SignApprovalTermsForm(forms.ModelForm):
378
    class Meta:
379
        model = AstakosUser
380
        fields = ("has_signed_terms",)
381

    
382
    def __init__(self, *args, **kwargs):
383
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
384

    
385
    def clean_has_signed_terms(self):
386
        has_signed_terms = self.cleaned_data['has_signed_terms']
387
        if not has_signed_terms:
388
            raise forms.ValidationError(_('You have to agree with the terms'))
389
        return has_signed_terms
390

    
391
class InvitationForm(forms.ModelForm):
392
    username = forms.EmailField(label=_("Email"))
393
    
394
    def __init__(self, *args, **kwargs):
395
        super(InvitationForm, self).__init__(*args, **kwargs)
396
    
397
    class Meta:
398
        model = Invitation
399
        fields = ('username', 'realname')
400
    
401
    def clean_username(self):
402
        username = self.cleaned_data['username']
403
        try:
404
            Invitation.objects.get(username = username)
405
            raise forms.ValidationError(_('There is already invitation for this email.'))
406
        except Invitation.DoesNotExist:
407
            pass
408
        return username