forms.py 37.9 KB
Newer Older
1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
2
#
3
4
5
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
6
#
7
8
9
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
10
#
11
12
13
14
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
15
#
16
17
18
19
20
21
22
23
24
25
26
27
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
28
#
29
30
31
32
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
33
from random import random
34
from datetime import datetime
35
36
37

from django import forms
from django.utils.translation import ugettext as _
38
39
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \
    PasswordResetForm, PasswordChangeForm, SetPasswordForm
40
from django.core.mail import send_mail, get_connection
41
from django.contrib.auth.tokens import default_token_generator
42
from django.core.urlresolvers import reverse
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
43
from django.utils.safestring import mark_safe
44
from django.utils.encoding import smart_str
45
from django.db import transaction
46
from django.core import validators
47

48
from synnefo.util import units
49
from synnefo_branding.utils import render_to_string
50
from synnefo.lib import join_urls
51
52
from astakos.im.models import AstakosUser, EmailChange, Invitation, Resource, \
    PendingThirdPartyUser, get_latest_terms, ProjectApplication, Project
53
from astakos.im import presentation
54
from astakos.im.widgets import DummyWidget, RecaptchaWidget
55
from astakos.im.functions import send_change_email, submit_application, \
56
    accept_membership_project_checks, ProjectError
57

58
from astakos.im.util import reserved_verified_email, model_to_dict
59
from astakos.im import auth_providers
60
from astakos.im import settings
61
from astakos.im import auth
62

Olga Brani's avatar
Olga Brani committed
63
import astakos.im.messages as astakos_messages
64

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
65
import logging
66
import hashlib
67
import recaptcha.client.captcha as captcha
68
import re
69

70
71
logger = logging.getLogger(__name__)

72
DOMAIN_VALUE_REGEX = re.compile(
73
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
74
    re.IGNORECASE)
75

76

77
class LocalUserCreationForm(UserCreationForm):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
78
79
    """
    Extends the built in UserCreationForm in several ways:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
80

81
82
    * Adds email, first_name, last_name, recaptcha_challenge_field,
    * recaptcha_response_field field.
83
    * The username field isn't visible and it is assigned a generated id.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
84
    * User created is not active.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
85
    """
86
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
87
88
    recaptcha_response_field = forms.CharField(
        widget=RecaptchaWidget, label='')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
89

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
90
91
    class Meta:
        model = AstakosUser
92
93
        fields = ("email", "first_name", "last_name",
                  "has_signed_terms", "has_signed_terms")
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
94

95
96
    def __init__(self, *args, **kwargs):
        """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
97
        Changes the order of fields, and removes the username field.
98
        """
99
        request = kwargs.pop('request', None)
100
101
102
103
104
105
        provider = kwargs.pop('provider', 'local')

        # we only use LocalUserCreationForm for local provider
        if not provider == 'local':
            raise Exception('Invalid provider')

106
        self.ip = None
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
107
108
        if request:
            self.ip = request.META.get('REMOTE_ADDR',
109
110
                                       request.META.get('HTTP_X_REAL_IP',
                                                        None))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
111

112
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
113
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
114
                                'password1', 'password2']
Olga Brani's avatar
Olga Brani committed
115

116
        if settings.RECAPTCHA_ENABLED:
117
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
118
                                         'recaptcha_response_field', ])
Olga Brani's avatar
Olga Brani committed
119
120
        if get_latest_terms():
            self.fields.keyOrder.append('has_signed_terms')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
121
122
123
124
125

        if 'has_signed_terms' in self.fields:
            # Overriding field label since we need to apply a link
            # to the terms within the label
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
126
                % (reverse('latest_terms'), _("the terms"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
127
            self.fields['has_signed_terms'].label = \
128
                mark_safe("I agree with %s" % terms_link_html)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
129

130
    def clean_email(self):
131
        email = self.cleaned_data['email']
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
132
        if not email:
Olga Brani's avatar
Olga Brani committed
133
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
134
        if reserved_verified_email(email):
Olga Brani's avatar
Olga Brani committed
135
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
136
        return email
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
137

138
139
140
    def clean_has_signed_terms(self):
        has_signed_terms = self.cleaned_data['has_signed_terms']
        if not has_signed_terms:
Olga Brani's avatar
Olga Brani committed
141
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
142
        return has_signed_terms
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
143

144
145
146
147
148
149
150
151
152
153
154
155
156
    def clean_recaptcha_response_field(self):
        if 'recaptcha_challenge_field' in self.cleaned_data:
            self.validate_captcha()
        return self.cleaned_data['recaptcha_response_field']

    def clean_recaptcha_challenge_field(self):
        if 'recaptcha_response_field' in self.cleaned_data:
            self.validate_captcha()
        return self.cleaned_data['recaptcha_challenge_field']

    def validate_captcha(self):
        rcf = self.cleaned_data['recaptcha_challenge_field']
        rrf = self.cleaned_data['recaptcha_response_field']
157
158
        check = captcha.submit(
            rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
159
        if not check.is_valid:
160
161
            raise forms.ValidationError(_(
                astakos_messages.CAPTCHA_VALIDATION_ERR))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
162

163
164
165
166
167
168
169
170
171
172
173
    def create_user(self):
        try:
            data = self.cleaned_data
        except AttributeError:
            self.is_valid()
            data = self.cleaned_data

        user = auth.make_local_user(
            email=data['email'], password=data['password1'],
            first_name=data['first_name'], last_name=data['last_name'],
            has_signed_terms=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
174
        return user
175

176

177
class ThirdPartyUserCreationForm(forms.ModelForm):
Olga Brani's avatar
Olga Brani committed
178
179
    email = forms.EmailField(
        label='Contact email',
180
181
        help_text='This is needed for contact purposes. '
        'It doesn&#39;t need to be the same with the one you '
Olga Brani's avatar
Olga Brani committed
182
        'provided to login previously. '
Olga Brani's avatar
Olga Brani committed
183
    )
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
184

185
186
    class Meta:
        model = AstakosUser
187
        fields = ['email', 'first_name', 'last_name', 'has_signed_terms']
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
188

189
190
191
192
    def __init__(self, *args, **kwargs):
        """
        Changes the order of fields, and removes the username field.
        """
193
194

        self.provider = kwargs.pop('provider', None)
195
        self.request = kwargs.pop('request', None)
196
197
198
199
200
201
202
203
204
        if not self.provider or self.provider == 'local':
            raise Exception('Invalid provider, %r' % self.provider)

        # ThirdPartyUserCreationForm should always get instantiated with
        # a third_party_token value
        self.third_party_token = kwargs.pop('third_party_token', None)
        if not self.third_party_token:
            raise Exception('ThirdPartyUserCreationForm'
                            ' requires third_party_token')
205

206
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
207

208
209
210
        if not get_latest_terms():
            del self.fields['has_signed_terms']

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
211
212
213
214
        if 'has_signed_terms' in self.fields:
            # Overriding field label since we need to apply a link
            # to the terms within the label
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
215
                % (reverse('latest_terms'), _("the terms"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
216
            self.fields['has_signed_terms'].label = \
217
                mark_safe("I agree with %s" % terms_link_html)
218

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
219
    def clean_email(self):
220
        email = self.cleaned_data['email']
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
221
        if not email:
Olga Brani's avatar
Olga Brani committed
222
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
223
        if reserved_verified_email(email):
224
            provider_id = self.provider
225
226
            provider = auth_providers.get_provider(provider_id)
            extra_message = provider.get_add_to_existing_account_msg
227

228
229
            raise forms.ValidationError(mark_safe(
                _(astakos_messages.EMAIL_USED) + ' ' + extra_message))
230
        return email
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
231

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
232
233
234
    def clean_has_signed_terms(self):
        has_signed_terms = self.cleaned_data['has_signed_terms']
        if not has_signed_terms:
Olga Brani's avatar
Olga Brani committed
235
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
236
        return has_signed_terms
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
237

238
239
240
    def _get_pending_user(self):
        return PendingThirdPartyUser.objects.get(token=self.third_party_token)

241
242
243
244
245
246
247
248
249
250
251
    def create_user(self):
        try:
            data = self.cleaned_data
        except AttributeError:
            self.is_valid()
            data = self.cleaned_data

        user = auth.make_user(
            email=data["email"],
            first_name=data["first_name"], last_name=data["last_name"],
            has_signed_terms=True)
252
        pending = self._get_pending_user()
253
254
255
        provider = pending.get_provider(user)
        provider.add_to_user()
        pending.delete()
256
257
        return user

258

259
260
class LoginForm(AuthenticationForm):
    username = forms.EmailField(label=_("Email"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
261
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
262
263
    recaptcha_response_field = forms.CharField(
        widget=RecaptchaWidget, label='')
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
264

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
265
266
267
268
    def __init__(self, *args, **kwargs):
        was_limited = kwargs.get('was_limited', False)
        request = kwargs.get('request', None)
        if request:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
269
270
271
            self.ip = request.META.get(
                'REMOTE_ADDR',
                request.META.get('HTTP_X_REAL_IP', None))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
272

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
273
274
275
276
277
        t = ('request', 'was_limited')
        for elem in t:
            if elem in kwargs.keys():
                kwargs.pop(elem)
        super(LoginForm, self).__init__(*args, **kwargs)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
278

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
279
        self.fields.keyOrder = ['username', 'password']
280
        if was_limited and settings.RECAPTCHA_ENABLED:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
281
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
282
                                         'recaptcha_response_field', ])
Olga Brani's avatar
Olga Brani committed
283
284

    def clean_username(self):
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
285
        return self.cleaned_data['username'].lower()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
286

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
287
288
289
290
291
292
293
294
295
296
297
298
299
    def clean_recaptcha_response_field(self):
        if 'recaptcha_challenge_field' in self.cleaned_data:
            self.validate_captcha()
        return self.cleaned_data['recaptcha_response_field']

    def clean_recaptcha_challenge_field(self):
        if 'recaptcha_response_field' in self.cleaned_data:
            self.validate_captcha()
        return self.cleaned_data['recaptcha_challenge_field']

    def validate_captcha(self):
        rcf = self.cleaned_data['recaptcha_challenge_field']
        rrf = self.cleaned_data['recaptcha_response_field']
300
301
        check = captcha.submit(
            rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
302
        if not check.is_valid:
303
304
            raise forms.ValidationError(_(
                astakos_messages.CAPTCHA_VALIDATION_ERR))
305

306
    def clean(self):
307
308
309
        """
        Override default behavior in order to check user's activation later
        """
310
311
        username = self.cleaned_data.get('username')

312
313
314
315
        if username:
            try:
                user = AstakosUser.objects.get_by_identifier(username)
                if not user.has_auth_provider('local'):
316
                    provider = auth_providers.get_provider('local', user)
317
318
                    msg = provider.get_login_disabled_msg
                    raise forms.ValidationError(mark_safe(msg))
319
320
            except AstakosUser.DoesNotExist:
                pass
321

322
323
        try:
            super(LoginForm, self).clean()
324
        except forms.ValidationError:
325
326
327
            if self.user_cache is None:
                raise
            if not self.user_cache.is_active:
328
329
                msg = self.user_cache.get_inactive_message('local')
                raise forms.ValidationError(msg)
330
331
332
            if self.request:
                if not self.request.session.test_cookie_worked():
                    raise
333
        return self.cleaned_data
334

335

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
336
337
338
class ProfileForm(forms.ModelForm):
    """
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
Olga Brani's avatar
Olga Brani committed
339
340
    Most of the fields are readonly since the user is not allowed to change
    them.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
341

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
342
343
    The class defines a save method which sets ``is_verified`` to True so as
    the user during the next login will not to be redirected to profile page.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
344
    """
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
345
346
    email = forms.EmailField(label='E-mail address',
                             help_text='E-mail address')
347
    renew = forms.BooleanField(label='Renew token', required=False)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
348

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
349
350
    class Meta:
        model = AstakosUser
351
        fields = ('email', 'first_name', 'last_name')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
352

353
    def __init__(self, *args, **kwargs):
354
        self.session_key = kwargs.pop('session_key', None)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
355
356
        super(ProfileForm, self).__init__(*args, **kwargs)
        instance = getattr(self, 'instance', None)
357
        ro_fields = ('email',)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
358
359
360
        if instance and instance.id:
            for field in ro_fields:
                self.fields[field].widget.attrs['readonly'] = True
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
361

362
363
364
    def clean_email(self):
        return self.instance.email

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
365
366
    def save(self, commit=True, **kwargs):
        user = super(ProfileForm, self).save(commit=False, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
367
        user.is_verified = True
368
        if self.cleaned_data.get('renew'):
369
370
371
372
            user.renew_token(
                flush_sessions=True,
                current_key=self.session_key
            )
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
373
        if commit:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
374
            user.save(**kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
375
        return user
376

377

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
378
379
380
381
class FeedbackForm(forms.Form):
    """
    Form for writing feedback.
    """
382
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
383
384
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
                                    required=False)
385

386

387
388
389
390
class SendInvitationForm(forms.Form):
    """
    Form for sending an invitations
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
391

392
393
394
395
    email = forms.EmailField(required=True, label='Email address')
    first_name = forms.EmailField(label='First name')
    last_name = forms.EmailField(label='Last name')

396
397
398

class ExtendedPasswordResetForm(PasswordResetForm):
    """
399
    Extends PasswordResetForm by overriding
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
400

401
402
    save method: to pass a custom from_email in send_mail.
    clean_email: to handle local auth provider checks
403
    """
404
    def clean_email(self):
405
406
407
        # we override the default django auth clean_email to provide more
        # detailed messages in case of inactive users
        email = self.cleaned_data['email']
408
        try:
409
            user = AstakosUser.objects.get_by_identifier(email)
410
            self.users_cache = [user]
411
            if not user.is_active:
412
413
                msg = mark_safe(user.get_inactive_message('local'))
                raise forms.ValidationError(msg)
414

415
            provider = auth_providers.get_provider('local', user)
416
            if not user.has_usable_password():
417
                msg = provider.get_unusable_password_msg
418
                raise forms.ValidationError(mark_safe(msg))
419
420

            if not user.can_change_password():
421
                msg = provider.get_cannot_change_password_msg
422
                raise forms.ValidationError(mark_safe(msg))
423
424

        except AstakosUser.DoesNotExist:
Olga Brani's avatar
Olga Brani committed
425
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
426
        return email
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
427

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
428
429
430
    def save(self, domain_override=None,
             email_template_name='registration/password_reset_email.html',
             use_https=False, token_generator=default_token_generator,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
431
             request=None, **kwargs):
432
        """
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
433
434
        Generates a one-use only link for resetting password and sends to the
        user.
435

436
437
        """
        for user in self.users_cache:
438
            url = user.astakosuser.get_password_reset_url(token_generator)
439
            url = join_urls(settings.BASE_HOST, url)
440
441
            c = {
                'email': user.email,
442
                'url': url,
443
                'site_name': settings.SITENAME,
444
                'user': user,
445
                'baseurl': settings.BASE_URL,
446
                'support': settings.CONTACT_EMAIL
447
            }
448
            message = render_to_string(email_template_name, c)
449
            from_email = settings.SERVER_EMAIL
450
            send_mail(_(astakos_messages.PASSWORD_RESET_EMAIL_SUBJECT),
451
                      message,
452
453
                      from_email,
                      [user.email],
454
                      connection=get_connection())
455

456

457
class EmailChangeForm(forms.ModelForm):
458

459
460
461
    class Meta:
        model = EmailChange
        fields = ('new_email_address',)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
462

463
464
    def clean_new_email_address(self):
        addr = self.cleaned_data['new_email_address']
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
465
        if reserved_verified_email(addr):
Olga Brani's avatar
Olga Brani committed
466
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
467
        return addr
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
468

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
469
470
    def save(self, request,
             email_template_name='registration/email_change_email.txt',
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
471
472
             commit=True, **kwargs):
        ec = super(EmailChangeForm, self).save(commit=False, **kwargs)
473
        ec.user = request.user
474
475
476
        # delete pending email changes
        request.user.emailchanges.all().delete()

477
478
479
        activation_key = hashlib.sha1(
            str(random()) + smart_str(ec.new_email_address))
        ec.activation_key = activation_key.hexdigest()
480
        if commit:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
481
            ec.save(**kwargs)
482
483
        send_change_email(ec, request, email_template_name=email_template_name)

484

485
class SignApprovalTermsForm(forms.ModelForm):
486

487
488
489
    class Meta:
        model = AstakosUser
        fields = ("has_signed_terms",)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
490

491
492
    def __init__(self, *args, **kwargs):
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
493

494
495
496
    def clean_has_signed_terms(self):
        has_signed_terms = self.cleaned_data['has_signed_terms']
        if not has_signed_terms:
Olga Brani's avatar
Olga Brani committed
497
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
498
        return has_signed_terms
499

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
500
501
    def save(self, commit=True, **kwargs):
        user = super(SignApprovalTermsForm, self).save(commit=commit, **kwargs)
502
503
        user.date_signed_terms = datetime.now()
        if commit:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
504
            user.save(**kwargs)
505
506
        return user

507

508
class InvitationForm(forms.ModelForm):
509

510
    username = forms.EmailField(label=_("Email"))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
511

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
512
513
    def __init__(self, *args, **kwargs):
        super(InvitationForm, self).__init__(*args, **kwargs)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
514

515
516
517
    class Meta:
        model = Invitation
        fields = ('username', 'realname')
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
518

519
520
521
    def clean_username(self):
        username = self.cleaned_data['username']
        try:
522
            Invitation.objects.get(username=username)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
523
524
            raise forms.ValidationError(
                _(astakos_messages.INVITATION_EMAIL_EXISTS))
525
526
        except Invitation.DoesNotExist:
            pass
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
527
        return username
528

529

530
531
532
533
534
class ExtendedPasswordChangeForm(PasswordChangeForm):
    """
    Extends PasswordChangeForm by enabling user
    to optionally renew also the token.
    """
535
    if not settings.NEWPASSWD_INVALIDATE_TOKEN:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
536
537
538
539
        renew = forms.BooleanField(
            label='Renew token', required=False,
            initial=True,
            help_text='Unsetting this may result in security risk.')
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
540

541
    def __init__(self, user, *args, **kwargs):
542
        self.session_key = kwargs.pop('session_key', None)
543
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
544

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
545
    def save(self, commit=True, **kwargs):
546
        try:
547
548
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
                    self.cleaned_data.get('renew'):
549
550
551
552
553
                self.user.renew_token()
            self.user.flush_sessions(current_key=self.session_key)
        except AttributeError:
            # if user model does has not such methods
            pass
554
        return super(ExtendedPasswordChangeForm, self).save(commit=commit,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
555
                                                            **kwargs)
556

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
557

558
559
560
561
562
class ExtendedSetPasswordForm(SetPasswordForm):
    """
    Extends SetPasswordForm by enabling user
    to optionally renew also the token.
    """
563
    if not settings.NEWPASSWD_INVALIDATE_TOKEN:
564
565
566
567
        renew = forms.BooleanField(
            label='Renew token',
            required=False,
            initial=True,
568
            help_text='Unsetting this may result in security risk.')
569

570
571
    def __init__(self, user, *args, **kwargs):
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
572
573

    @transaction.commit_on_success()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
574
    def save(self, commit=True, **kwargs):
575
576
        try:
            self.user = AstakosUser.objects.get(id=self.user.id)
577
578
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
                    self.cleaned_data.get('renew'):
579
                self.user.renew_token()
580
581
582
583

            provider = auth_providers.get_provider('local', self.user)
            if provider.get_add_policy:
                provider.add_to_user()
584

585
586
        except BaseException, e:
            logger.exception(e)
587
        return super(ExtendedSetPasswordForm, self).save(commit=commit,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
588
                                                         **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
589
590


Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
591
app_name_label = "Project name"
592
app_name_placeholder = _("myproject.mylab.ntua.gr")
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
593
594
595
596
597
app_name_validator = validators.RegexValidator(
    DOMAIN_VALUE_REGEX,
    _(astakos_messages.DOMAIN_VALUE_ERR),
    'invalid')
app_name_help = _("""
598
        The project's name should be in a domain format.
599
600
601
602
        The domain shouldn't neccessarily exist in the real
        world but is helpful to imply a structure.
        e.g.: myproject.mylab.ntua.gr or
        myservice.myteam.myorganization""")
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
603
604
app_name_widget = forms.TextInput(
    attrs={'placeholder': app_name_placeholder})
605
606


Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
607
608
609
app_home_label = "Homepage URL"
app_home_placeholder = 'myinstitution.org/myproject/'
app_home_help = _("""
610
        URL pointing at your project's site.
Olga Brani's avatar
Olga Brani committed
611
        e.g.: myinstitution.org/myproject/.
612
        Leave blank if there is no website.""")
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
613
614
app_home_widget = forms.TextInput(
    attrs={'placeholder': app_home_placeholder})
615

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
616
617
app_desc_label = _("Description")
app_desc_help = _("""
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
618
        Please provide a short but descriptive abstract of your
619
620
        project, so that anyone searching can quickly understand
        what this project is about.""")
621

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
622
623
app_comment_label = _("Comments for review (private)")
app_comment_help = _("""
624
625
626
627
        Write down any comments you may have for the reviewer
        of this application (e.g. background and rationale to
        support your request).
        The comments are strictly for the review process
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
628
        and will not be made public.""")
629

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
630
631
app_start_date_label = _("Start date")
app_start_date_help = _("""
632
633
634
635
        Provide a date when your need your project to be created,
        and members to be able to join and get resources.
        This date is only a hint to help prioritize reviews.""")

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
636
637
app_end_date_label = _("Termination date")
app_end_date_help = _("""
638
        At this date, the project will be automatically terminated
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
639
640
        and its resource grants revoked from all members. If you are
        not certain, it is best to start with a conservative estimation.
641
642
        You can always re-apply for an extension, if you need.""")

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
643
644
join_policy_label = _("Joining policy")
app_member_join_policy_help = _("""
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
645
        Select how new members are accepted into the project.""")
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
646
647
leave_policy_label = _("Leaving policy")
app_member_leave_policy_help = _("""
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
648
        Select how new members can leave the project.""")
649

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
650
651
max_members_label = _("Maximum member count")
max_members_help = _("""
652
653
654
        Specify the maximum number of members this project may have,
        including the owner. Beyond this number, no new members
        may join the project and be granted the project resources.
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
655
656
        If you are not certain, it is best to start with a conservative
        limit. You can always request a raise when you need it.""")
657

658
659
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
660

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
661

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
662
class ProjectApplicationForm(forms.ModelForm):
663

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
664
    name = forms.CharField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
665
666
667
668
        label=app_name_label,
        help_text=app_name_help,
        widget=app_name_widget,
        validators=[app_name_validator])
669

670
    homepage = forms.URLField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
671
672
673
674
        label=app_home_label,
        help_text=app_home_help,
        widget=app_home_widget,
        required=False)
675
676

    description = forms.CharField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
677
678
679
680
        label=app_desc_label,
        help_text=app_desc_help,
        widget=forms.Textarea,
        required=False)
681
682

    comments = forms.CharField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
683
684
685
686
        label=app_comment_label,
        help_text=app_comment_help,
        widget=forms.Textarea,
        required=False)
687
688

    start_date = forms.DateTimeField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
689
690
691
        label=app_start_date_label,
        help_text=app_start_date_help,
        required=False)
692
693

    end_date = forms.DateTimeField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
694
695
        label=app_end_date_label,
        help_text=app_end_date_help)
696

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
697
698
699
700
701
702
    member_join_policy = forms.TypedChoiceField(
        label=join_policy_label,
        help_text=app_member_join_policy_help,
        initial=2,
        coerce=int,
        choices=join_policies)
703

704
    member_leave_policy = forms.TypedChoiceField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
705
706
707
708
        label=leave_policy_label,
        help_text=app_member_leave_policy_help,
        coerce=int,
        choices=leave_policies)
709
710

    limit_on_members_number = forms.IntegerField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
711
712
713
        label=max_members_label,
        help_text=max_members_help,
        min_value=0,
714
        required=True)
715

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
716
    class Meta:
717
        model = ProjectApplication
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
718
719
720
721
        fields = ('name', 'homepage', 'description',
                  'start_date', 'end_date', 'comments',
                  'member_join_policy', 'member_leave_policy',
                  'limit_on_members_number')
722

723
    def __init__(self, *args, **kwargs):
724
725
        instance = kwargs.get('instance')
        self.precursor_application = instance
726
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
727
        # in case of new application remove closed join policy
728
        if not instance:
729
730
            policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.copy()
            policies.pop(3)
731
            self.fields['member_join_policy'].choices = policies.iteritems()
732

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
733
734
    def clean_start_date(self):
        start_date = self.cleaned_data.get('start_date')
735
        if not self.precursor_application:
736
737
            today = datetime.now()
            today = datetime(today.year, today.month, today.day)
738
            if start_date and (start_date - today).days < 0:
739
                raise forms.ValidationError(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
740
                    _(astakos_messages.INVALID_PROJECT_START_DATE))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
741
742
743
744
745
        return start_date

    def clean_end_date(self):
        start_date = self.cleaned_data.get('start_date')
        end_date = self.cleaned_data.get('end_date')
746
747
        today = datetime.now()
        today = datetime(today.year, today.month, today.day)
748
        if end_date and (end_date - today).days < 0:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
749
750
            raise forms.ValidationError(
                _(astakos_messages.INVALID_PROJECT_END_DATE))
751
        if start_date and (end_date - start_date).days <= 0:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
752
753
754
755
            raise forms.ValidationError(
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
        return end_date

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
756
    def clean(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
757
        userid = self.data.get('user', None)
758
        self.resource_policies
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
759
760
761
762
763
764
765
766
767
768
        self.user = None
        if userid:
            try:
                self.user = AstakosUser.objects.get(id=userid)
            except AstakosUser.DoesNotExist:
                pass
        if not self.user:
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
        super(ProjectApplicationForm, self).clean()
        return self.cleaned_data
769

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
770
771
    @property
    def resource_policies(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
772
773
        policies = []
        append = policies.append
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
774
775
776
        for name, value in self.data.iteritems():
            if not value:
                continue
777
778
779
            uplimit = value
            if name.endswith('_uplimit'):
                subs = name.split('_uplimit')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
780
                prefix, suffix = subs
781
782
783
784
785
                try:
                    resource = Resource.objects.get(name=prefix)
                except Resource.DoesNotExist:
                    raise forms.ValidationError("Resource %s does not exist" %
                                                resource.name)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
786
                # keep only resource limits for selected resource groups
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
787
788
                if self.data.get('is_selected_%s' %
                                 resource.group, "0") == "1":
789
                    if not resource.ui_visible:
790
791
                        raise forms.ValidationError("Invalid resource %s" %
                                                    resource.name)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
792
                    d = model_to_dict(resource)
793
794
795
796
797
798
799
800
                    try:
                        uplimit = long(uplimit)
                    except ValueError:
                        m = "Limit should be an integer"
                        raise forms.ValidationError(m)
                    display = units.show(uplimit, resource.unit)
                    d.update(dict(resource=prefix, uplimit=uplimit,
                                  display_uplimit=display))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
801
                    append(d)
802

803
        ordered_keys = presentation.RESOURCES['resources_order']
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
804

805
806
807
808
809
810
811
        def resource_order(r):
            if r['str_repr'] in ordered_keys:
                return ordered_keys.index(r['str_repr'])
            else:
                return -1

        policies = sorted(policies, key=resource_order)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
812
        return policies
813

814
    def cleaned_resource_policies(self):
815
816
817
818
819
820
821
822
        policies = {}
        for d in self.resource_policies:
            policies[d["name"]] = {
                "project_capacity": None,
                "member_capacity": d["uplimit"]
            }

        return policies
823

Ilias Tsitsimpis's avatar