Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 18ffbee1

History | View | Annotate | Download (14.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
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, BASEURL, 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
        self.fields['inviter'].widget.attrs['readonly'] = True
164
        self.fields['email'].widget.attrs['readonly'] = True
165
        self.fields['username'].widget.attrs['readonly'] = True
166

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

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

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

    
239
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
240
    def clean_email(self):
241
        email = self.cleaned_data['email']
242
        if not email:
243
            raise forms.ValidationError(_("This field is required"))
244
        try:
245
            user = AstakosUser.objects.get(email = email)
246
            if user.provider == 'local':
247
                self.instance = user
248
                return email
249
            else:
250
                raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
251
        except AstakosUser.DoesNotExist:
252
            return email
253
    
254
class LoginForm(AuthenticationForm):
255
    username = forms.EmailField(label=_("Email"))
256

    
257
class ProfileForm(forms.ModelForm):
258
    """
259
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
260
    Most of the fields are readonly since the user is not allowed to change them.
261

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

    
267
    class Meta:
268
        model = AstakosUser
269
        fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires', 'groups')
270

    
271
    def __init__(self, *args, **kwargs):
272
        super(ProfileForm, self).__init__(*args, **kwargs)
273
        instance = getattr(self, 'instance', None)
274
        ro_fields = ('auth_token', 'auth_token_expires', 'groups')
275
        if instance and instance.id:
276
            for field in ro_fields:
277
                self.fields[field].widget.attrs['readonly'] = True
278

    
279
    def save(self, commit=True):
280
        user = super(ProfileForm, self).save(commit=False)
281
        user.is_verified = True
282
        if self.cleaned_data.get('renew'):
283
            user.renew_token()
284
        if commit:
285
            user.save()
286
        return user
287

    
288
class FeedbackForm(forms.Form):
289
    """
290
    Form for writing feedback.
291
    """
292
    feedback_msg = forms.CharField(widget=forms.TextInput(), label=u'Message')
293
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
294
                                    required=False)
295

    
296
class SendInvitationForm(forms.Form):
297
    """
298
    Form for sending an invitations
299
    """
300

    
301
    email = forms.EmailField(required = True, label = 'Email address')
302
    first_name = forms.EmailField(label = 'First name')
303
    last_name = forms.EmailField(label = 'Last name')
304

    
305
class ExtendedPasswordResetForm(PasswordResetForm):
306
    """
307
    Extends PasswordResetForm by overriding save method:
308
    passes a custom from_email in send_mail.
309

310
    Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
311
    accepts a from_email argument.
312
    """
313
    def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
314
             use_https=False, token_generator=default_token_generator, request=None):
315
        """
316
        Generates a one-use only link for resetting password and sends to the user.
317
        """
318
        for user in self.users_cache:
319
            url = reverse('django.contrib.auth.views.password_reset_confirm',
320
                          kwargs={'uidb36':int_to_base36(user.id),
321
                                  'token':token_generator.make_token(user)})
322
            url = request.build_absolute_uri(url)
323
            t = loader.get_template(email_template_name)
324
            c = {
325
                'email': user.email,
326
                'url': url,
327
                'site_name': SITENAME,
328
                'user': user,
329
                'baseurl': BASEURL,
330
                'support': DEFAULT_CONTACT_EMAIL
331
            }
332
            from_email = DEFAULT_FROM_EMAIL
333
            send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
334
                t.render(Context(c)), from_email, [user.email])
335

    
336
class SignApprovalTermsForm(forms.ModelForm):
337
    class Meta:
338
        model = AstakosUser
339
        fields = ("has_signed_terms",)
340

    
341
    def __init__(self, *args, **kwargs):
342
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
343

    
344
    def clean_has_signed_terms(self):
345
        has_signed_terms = self.cleaned_data['has_signed_terms']
346
        if not has_signed_terms:
347
            raise forms.ValidationError(_('You have to agree with the terms'))
348
        return has_signed_terms
349

    
350
class InvitationForm(forms.ModelForm):
351
    username = forms.EmailField(label=_("Email"))
352
    
353
    def __init__(self, *args, **kwargs):
354
        super(InvitationForm, self).__init__(*args, **kwargs)
355
    
356
    class Meta:
357
        model = Invitation
358
        fields = ('username', 'realname')
359
    
360
    def clean_username(self):
361
        username = self.cleaned_data['username']
362
        try:
363
            Invitation.objects.get(username = username)
364
            raise forms.ValidationError(_('There is already invitation for this email.'))
365
        except Invitation.DoesNotExist:
366
            pass
367
        return username