Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (41.3 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 random import random
34
from datetime import datetime
35

    
36
from django import forms
37
from django.utils.translation import ugettext as _
38
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \
39
    PasswordResetForm, PasswordChangeForm, 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
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
from django.core.exceptions import PermissionDenied
48

    
49
from synnefo_branding.utils import render_to_string
50
from synnefo.lib import join_urls
51
from astakos.im.models import AstakosUser, EmailChange, Invitation, Resource, \
52
    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_checks
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

    
62
import astakos.im.messages as astakos_messages
63

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

    
69
logger = logging.getLogger(__name__)
70

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

    
75

    
76
class StoreUserMixin(object):
77

    
78
    def store_user(self, user, request=None):
79
        """
80
        WARNING: this should be wrapped inside a transactional view/method.
81
        """
82
        user.save()
83
        self.post_store_user(user, request)
84
        return user
85

    
86
    def post_store_user(self, user, request):
87
        """
88
        Interface method for descendant backends to be able to do stuff within
89
        the transaction enabled by store_user.
90
        """
91
        pass
92

    
93

    
94
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
95
    """
96
    Extends the built in UserCreationForm in several ways:
97

98
    * Adds email, first_name, last_name, recaptcha_challenge_field,
99
    * recaptcha_response_field field.
100
    * The username field isn't visible and it is assigned a generated id.
101
    * User created is not active.
102
    """
103
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
104
    recaptcha_response_field = forms.CharField(
105
        widget=RecaptchaWidget, label='')
106

    
107
    class Meta:
108
        model = AstakosUser
109
        fields = ("email", "first_name", "last_name",
110
                  "has_signed_terms", "has_signed_terms")
111

    
112
    def __init__(self, *args, **kwargs):
113
        """
114
        Changes the order of fields, and removes the username field.
115
        """
116
        request = kwargs.pop('request', None)
117
        provider = kwargs.pop('provider', 'local')
118

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

    
123
        self.ip = None
124
        if request:
125
            self.ip = request.META.get('REMOTE_ADDR',
126
                                       request.META.get('HTTP_X_REAL_IP',
127
                                                        None))
128

    
129
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
130
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
131
                                'password1', 'password2']
132

    
133
        if settings.RECAPTCHA_ENABLED:
134
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
135
                                         'recaptcha_response_field', ])
136
        if get_latest_terms():
137
            self.fields.keyOrder.append('has_signed_terms')
138

    
139
        if 'has_signed_terms' in self.fields:
140
            # Overriding field label since we need to apply a link
141
            # to the terms within the label
142
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
143
                % (reverse('latest_terms'), _("the terms"))
144
            self.fields['has_signed_terms'].label = \
145
                mark_safe("I agree with %s" % terms_link_html)
146

    
147
    def clean_email(self):
148
        email = self.cleaned_data['email']
149
        if not email:
150
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
151
        if reserved_verified_email(email):
152
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
153
        return email
154

    
155
    def clean_has_signed_terms(self):
156
        has_signed_terms = self.cleaned_data['has_signed_terms']
157
        if not has_signed_terms:
158
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
159
        return has_signed_terms
160

    
161
    def clean_recaptcha_response_field(self):
162
        if 'recaptcha_challenge_field' in self.cleaned_data:
163
            self.validate_captcha()
164
        return self.cleaned_data['recaptcha_response_field']
165

    
166
    def clean_recaptcha_challenge_field(self):
167
        if 'recaptcha_response_field' in self.cleaned_data:
168
            self.validate_captcha()
169
        return self.cleaned_data['recaptcha_challenge_field']
170

    
171
    def validate_captcha(self):
172
        rcf = self.cleaned_data['recaptcha_challenge_field']
173
        rrf = self.cleaned_data['recaptcha_response_field']
174
        check = captcha.submit(
175
            rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
176
        if not check.is_valid:
177
            raise forms.ValidationError(_(
178
                astakos_messages.CAPTCHA_VALIDATION_ERR))
179

    
180
    def post_store_user(self, user, request=None):
181
        """
182
        Interface method for descendant backends to be able to do stuff within
183
        the transaction enabled by store_user.
184
        """
185
        user.add_auth_provider('local', auth_backend='astakos')
186
        user.set_password(self.cleaned_data['password1'])
187

    
188
    def save(self, commit=True, **kwargs):
189
        """
190
        Saves the email, first_name and last_name properties, after the normal
191
        save behavior is complete.
192
        """
193
        user = super(LocalUserCreationForm, self).save(commit=False, **kwargs)
194
        user.date_signed_terms = datetime.now()
195
        user.renew_token()
196
        if commit:
197
            user.save(**kwargs)
198
            logger.info('Created user %s', user.log_display)
199
        return user
200

    
201

    
202
class InvitedLocalUserCreationForm(LocalUserCreationForm):
203
    """
204
    Extends the LocalUserCreationForm: email is readonly.
205
    """
206
    class Meta:
207
        model = AstakosUser
208
        fields = ("email", "first_name", "last_name", "has_signed_terms")
209

    
210
    def __init__(self, *args, **kwargs):
211
        """
212
        Changes the order of fields, and removes the username field.
213
        """
214
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
215

    
216
        #set readonly form fields
217
        ro = ('email', 'username',)
218
        for f in ro:
219
            self.fields[f].widget.attrs['readonly'] = True
220

    
221
    def save(self, commit=True, **kwargs):
222
        user = super(InvitedLocalUserCreationForm, self).save(commit=False,
223
                                                              **kwargs)
224
        user.set_invitations_level()
225
        user.email_verified = True
226
        if commit:
227
            user.save(**kwargs)
228
        return user
229

    
230

    
231
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
232
    email = forms.EmailField(
233
        label='Contact email',
234
        help_text='This is needed for contact purposes. '
235
        'It doesn&#39;t need to be the same with the one you '
236
        'provided to login previously. '
237
    )
238

    
239
    class Meta:
240
        model = AstakosUser
241
        fields = ['email', 'first_name', 'last_name', 'has_signed_terms']
242

    
243
    def __init__(self, *args, **kwargs):
244
        """
245
        Changes the order of fields, and removes the username field.
246
        """
247

    
248
        self.provider = kwargs.pop('provider', None)
249
        self.request = kwargs.pop('request', None)
250
        if not self.provider or self.provider == 'local':
251
            raise Exception('Invalid provider, %r' % self.provider)
252

    
253
        # ThirdPartyUserCreationForm should always get instantiated with
254
        # a third_party_token value
255
        self.third_party_token = kwargs.pop('third_party_token', None)
256
        if not self.third_party_token:
257
            raise Exception('ThirdPartyUserCreationForm'
258
                            ' requires third_party_token')
259

    
260
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
261

    
262
        if not get_latest_terms():
263
            del self.fields['has_signed_terms']
264

    
265
        if 'has_signed_terms' in self.fields:
266
            # Overriding field label since we need to apply a link
267
            # to the terms within the label
268
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
269
                % (reverse('latest_terms'), _("the terms"))
270
            self.fields['has_signed_terms'].label = \
271
                mark_safe("I agree with %s" % terms_link_html)
272

    
273
    def clean_email(self):
274
        email = self.cleaned_data['email']
275
        if not email:
276
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
277
        if reserved_verified_email(email):
278
            provider_id = self.provider
279
            provider = auth_providers.get_provider(provider_id)
280
            extra_message = provider.get_add_to_existing_account_msg
281

    
282
            raise forms.ValidationError(mark_safe(
283
                _(astakos_messages.EMAIL_USED) + ' ' + extra_message))
284
        return email
285

    
286
    def clean_has_signed_terms(self):
287
        has_signed_terms = self.cleaned_data['has_signed_terms']
288
        if not has_signed_terms:
289
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
290
        return has_signed_terms
291

    
292
    def _get_pending_user(self):
293
        return PendingThirdPartyUser.objects.get(token=self.third_party_token)
294

    
295
    def post_store_user(self, user, request=None):
296
        pending = self._get_pending_user()
297
        provider = pending.get_provider(user)
298
        provider.add_to_user()
299
        pending.delete()
300

    
301
    def save(self, commit=True, **kwargs):
302
        user = super(ThirdPartyUserCreationForm, self).save(commit=False,
303
                                                            **kwargs)
304
        user.set_unusable_password()
305
        user.renew_token()
306
        user.date_signed_terms = datetime.now()
307
        if commit:
308
            user.save(**kwargs)
309
            logger.info('Created user %s' % user.log_display)
310
        return user
311

    
312

    
313
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
314
    """
315
    Extends the ThirdPartyUserCreationForm: email is readonly.
316
    """
317
    def __init__(self, *args, **kwargs):
318
        """
319
        Changes the order of fields, and removes the username field.
320
        """
321
        super(
322
            InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
323

    
324
        #set readonly form fields
325
        ro = ('email',)
326
        for f in ro:
327
            self.fields[f].widget.attrs['readonly'] = True
328

    
329
    def save(self, commit=True, **kwargs):
330
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False,
331
                                                                   **kwargs)
332
        user.set_invitation_level()
333
        user.email_verified = True
334
        if commit:
335
            user.save(**kwargs)
336
        return user
337

    
338

    
339
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
340
    additional_email = forms.CharField(
341
        widget=forms.HiddenInput(), label='', required=False)
342

    
343
    def __init__(self, *args, **kwargs):
344
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
345
        # copy email value to additional_mail in case user will change it
346
        name = 'email'
347
        field = self.fields[name]
348
        self.initial['additional_email'] = self.initial.get(name, field.initial)
349
        self.initial['email'] = None
350

    
351

    
352
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
353
                                        InvitedThirdPartyUserCreationForm):
354
    pass
355

    
356

    
357
class LoginForm(AuthenticationForm):
358
    username = forms.EmailField(label=_("Email"))
359
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
360
    recaptcha_response_field = forms.CharField(
361
        widget=RecaptchaWidget, label='')
362

    
363
    def __init__(self, *args, **kwargs):
364
        was_limited = kwargs.get('was_limited', False)
365
        request = kwargs.get('request', None)
366
        if request:
367
            self.ip = request.META.get('REMOTE_ADDR',
368
                                       request.META.get('HTTP_X_REAL_IP', None))
369

    
370
        t = ('request', 'was_limited')
371
        for elem in t:
372
            if elem in kwargs.keys():
373
                kwargs.pop(elem)
374
        super(LoginForm, self).__init__(*args, **kwargs)
375

    
376
        self.fields.keyOrder = ['username', 'password']
377
        if was_limited and settings.RECAPTCHA_ENABLED:
378
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
379
                                         'recaptcha_response_field', ])
380

    
381
    def clean_username(self):
382
        return self.cleaned_data['username'].lower()
383

    
384
    def clean_recaptcha_response_field(self):
385
        if 'recaptcha_challenge_field' in self.cleaned_data:
386
            self.validate_captcha()
387
        return self.cleaned_data['recaptcha_response_field']
388

    
389
    def clean_recaptcha_challenge_field(self):
390
        if 'recaptcha_response_field' in self.cleaned_data:
391
            self.validate_captcha()
392
        return self.cleaned_data['recaptcha_challenge_field']
393

    
394
    def validate_captcha(self):
395
        rcf = self.cleaned_data['recaptcha_challenge_field']
396
        rrf = self.cleaned_data['recaptcha_response_field']
397
        check = captcha.submit(
398
            rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
399
        if not check.is_valid:
400
            raise forms.ValidationError(_(
401
                astakos_messages.CAPTCHA_VALIDATION_ERR))
402

    
403
    def clean(self):
404
        """
405
        Override default behavior in order to check user's activation later
406
        """
407
        username = self.cleaned_data.get('username')
408

    
409
        if username:
410
            try:
411
                user = AstakosUser.objects.get_by_identifier(username)
412
                if not user.has_auth_provider('local'):
413
                    provider = auth_providers.get_provider('local', user)
414
                    msg = provider.get_login_disabled_msg
415
                    raise forms.ValidationError(mark_safe(msg))
416
            except AstakosUser.DoesNotExist:
417
                pass
418

    
419
        try:
420
            super(LoginForm, self).clean()
421
        except forms.ValidationError, e:
422
            if self.user_cache is None:
423
                raise
424
            if not self.user_cache.is_active:
425
                msg = self.user_cache.get_inactive_message('local')
426
                raise forms.ValidationError(msg)
427
            if self.request:
428
                if not self.request.session.test_cookie_worked():
429
                    raise
430
        return self.cleaned_data
431

    
432

    
433
class ProfileForm(forms.ModelForm):
434
    """
435
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
436
    Most of the fields are readonly since the user is not allowed to change
437
    them.
438

439
    The class defines a save method which sets ``is_verified`` to True so as the
440
    user during the next login will not to be redirected to profile page.
441
    """
442
    email = forms.EmailField(label='E-mail address', help_text='E-mail address')
443
    renew = forms.BooleanField(label='Renew token', required=False)
444

    
445
    class Meta:
446
        model = AstakosUser
447
        fields = ('email', 'first_name', 'last_name')
448

    
449
    def __init__(self, *args, **kwargs):
450
        self.session_key = kwargs.pop('session_key', None)
451
        super(ProfileForm, self).__init__(*args, **kwargs)
452
        instance = getattr(self, 'instance', None)
453
        ro_fields = ('email',)
454
        if instance and instance.id:
455
            for field in ro_fields:
456
                self.fields[field].widget.attrs['readonly'] = True
457

    
458
    def clean_email(self):
459
        return self.instance.email
460

    
461
    def save(self, commit=True, **kwargs):
462
        user = super(ProfileForm, self).save(commit=False, **kwargs)
463
        user.is_verified = True
464
        if self.cleaned_data.get('renew'):
465
            user.renew_token(
466
                flush_sessions=True,
467
                current_key=self.session_key
468
            )
469
        if commit:
470
            user.save(**kwargs)
471
        return user
472

    
473

    
474

    
475
class FeedbackForm(forms.Form):
476
    """
477
    Form for writing feedback.
478
    """
479
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
480
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
481
                                    required=False)
482

    
483

    
484
class SendInvitationForm(forms.Form):
485
    """
486
    Form for sending an invitations
487
    """
488

    
489
    email = forms.EmailField(required=True, label='Email address')
490
    first_name = forms.EmailField(label='First name')
491
    last_name = forms.EmailField(label='Last name')
492

    
493

    
494
class ExtendedPasswordResetForm(PasswordResetForm):
495
    """
496
    Extends PasswordResetForm by overriding
497

498
    save method: to pass a custom from_email in send_mail.
499
    clean_email: to handle local auth provider checks
500
    """
501
    def clean_email(self):
502
        # we override the default django auth clean_email to provide more
503
        # detailed messages in case of inactive users
504
        email = self.cleaned_data['email']
505
        try:
506
            user = AstakosUser.objects.get_by_identifier(email)
507
            self.users_cache = [user]
508
            if not user.is_active:
509
                msg = mark_safe(user.get_inactive_message('local'))
510
                raise forms.ValidationError(msg)
511

    
512
            provider = auth_providers.get_provider('local', user)
513
            if not user.has_usable_password():
514
                msg = provider.get_unusable_password_msg
515
                raise forms.ValidationError(mark_safe(msg))
516

    
517
            if not user.can_change_password():
518
                msg = provider.get_cannot_change_password_msg
519
                raise forms.ValidationError(mark_safe(msg))
520

    
521
        except AstakosUser.DoesNotExist:
522
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
523
        return email
524

    
525
    def save(self, domain_override=None,
526
             email_template_name='registration/password_reset_email.html',
527
             use_https=False, token_generator=default_token_generator,
528
             request=None, **kwargs):
529
        """
530
        Generates a one-use only link for resetting password
531
        and sends to the user.
532
        """
533
        for user in self.users_cache:
534
            url = user.astakosuser.get_password_reset_url(token_generator)
535
            url = join_urls(settings.BASE_HOST, url)
536
            c = {
537
                'email': user.email,
538
                'url': url,
539
                'site_name': settings.SITENAME,
540
                'user': user,
541
                'baseurl': settings.BASE_URL,
542
                'support': settings.CONTACT_EMAIL
543
            }
544
            message = render_to_string(email_template_name, c)
545
            from_email = settings.SERVER_EMAIL
546
            send_mail(_(astakos_messages.PASSWORD_RESET_EMAIL_SUBJECT),
547
                      message,
548
                      from_email,
549
                      [user.email],
550
                      connection=get_connection())
551

    
552

    
553
class EmailChangeForm(forms.ModelForm):
554

    
555
    class Meta:
556
        model = EmailChange
557
        fields = ('new_email_address',)
558

    
559
    def clean_new_email_address(self):
560
        addr = self.cleaned_data['new_email_address']
561
        if reserved_verified_email(addr):
562
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
563
        return addr
564

    
565
    def save(self, request,
566
             email_template_name='registration/email_change_email.txt',
567
             commit=True, **kwargs):
568
        ec = super(EmailChangeForm, self).save(commit=False, **kwargs)
569
        ec.user = request.user
570
        # delete pending email changes
571
        request.user.emailchanges.all().delete()
572

    
573
        activation_key = hashlib.sha1(
574
            str(random()) + smart_str(ec.new_email_address))
575
        ec.activation_key = activation_key.hexdigest()
576
        if commit:
577
            ec.save(**kwargs)
578
        send_change_email(ec, request, email_template_name=email_template_name)
579

    
580

    
581
class SignApprovalTermsForm(forms.ModelForm):
582

    
583
    class Meta:
584
        model = AstakosUser
585
        fields = ("has_signed_terms",)
586

    
587
    def __init__(self, *args, **kwargs):
588
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
589

    
590
    def clean_has_signed_terms(self):
591
        has_signed_terms = self.cleaned_data['has_signed_terms']
592
        if not has_signed_terms:
593
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
594
        return has_signed_terms
595

    
596
    def save(self, commit=True, **kwargs):
597
        user = super(SignApprovalTermsForm, self).save(commit=commit, **kwargs)
598
        user.date_signed_terms = datetime.now()
599
        if commit:
600
            user.save(**kwargs)
601
        return user
602

    
603

    
604
class InvitationForm(forms.ModelForm):
605

    
606
    username = forms.EmailField(label=_("Email"))
607

    
608
    def __init__(self, *args, **kwargs):
609
        super(InvitationForm, self).__init__(*args, **kwargs)
610

    
611
    class Meta:
612
        model = Invitation
613
        fields = ('username', 'realname')
614

    
615
    def clean_username(self):
616
        username = self.cleaned_data['username']
617
        try:
618
            Invitation.objects.get(username=username)
619
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
620
        except Invitation.DoesNotExist:
621
            pass
622
        return username
623

    
624

    
625
class ExtendedPasswordChangeForm(PasswordChangeForm):
626
    """
627
    Extends PasswordChangeForm by enabling user
628
    to optionally renew also the token.
629
    """
630
    if not settings.NEWPASSWD_INVALIDATE_TOKEN:
631
        renew = forms.BooleanField(label='Renew token', required=False,
632
                                   initial=True,
633
                                   help_text='Unsetting this may result in security risk.')
634

    
635
    def __init__(self, user, *args, **kwargs):
636
        self.session_key = kwargs.pop('session_key', None)
637
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
638

    
639
    def save(self, commit=True, **kwargs):
640
        try:
641
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
642
                    self.cleaned_data.get('renew'):
643
                self.user.renew_token()
644
            self.user.flush_sessions(current_key=self.session_key)
645
        except AttributeError:
646
            # if user model does has not such methods
647
            pass
648
        return super(ExtendedPasswordChangeForm, self).save(commit=commit,
649
                                                            **kwargs)
650

    
651
class ExtendedSetPasswordForm(SetPasswordForm):
652
    """
653
    Extends SetPasswordForm by enabling user
654
    to optionally renew also the token.
655
    """
656
    if not settings.NEWPASSWD_INVALIDATE_TOKEN:
657
        renew = forms.BooleanField(
658
            label='Renew token',
659
            required=False,
660
            initial=True,
661
            help_text='Unsetting this may result in security risk.')
662

    
663
    def __init__(self, user, *args, **kwargs):
664
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
665

    
666
    @transaction.commit_on_success()
667
    def save(self, commit=True, **kwargs):
668
        try:
669
            self.user = AstakosUser.objects.get(id=self.user.id)
670
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
671
                    self.cleaned_data.get('renew'):
672
                self.user.renew_token()
673

    
674
            provider = auth_providers.get_provider('local', self.user)
675
            if provider.get_add_policy:
676
                provider.add_to_user()
677

    
678
        except BaseException, e:
679
            logger.exception(e)
680
        return super(ExtendedSetPasswordForm, self).save(commit=commit,
681
                                                         **kwargs)
682

    
683

    
684

    
685

    
686
app_name_label       =  "Project name"
687
app_name_placeholder = _("myproject.mylab.ntua.gr")
688
app_name_validator   =  validators.RegexValidator(
689
                            DOMAIN_VALUE_REGEX,
690
                            _(astakos_messages.DOMAIN_VALUE_ERR),
691
                            'invalid')
692
app_name_help        =  _("""
693
        The project's name should be in a domain format.
694
        The domain shouldn't neccessarily exist in the real
695
        world but is helpful to imply a structure.
696
        e.g.: myproject.mylab.ntua.gr or
697
        myservice.myteam.myorganization""")
698
app_name_widget      =  forms.TextInput(
699
                            attrs={'placeholder': app_name_placeholder})
700

    
701

    
702
app_home_label       =  "Homepage URL"
703
app_home_placeholder =  'myinstitution.org/myproject/'
704
app_home_help        =  _("""
705
        URL pointing at your project's site.
706
        e.g.: myinstitution.org/myproject/.
707
        Leave blank if there is no website.""")
708
app_home_widget      =  forms.TextInput(
709
                            attrs={'placeholder': app_home_placeholder})
710

    
711
app_desc_label       =  _("Description")
712
app_desc_help        =  _("""
713
        Please provide a short but descriptive abstract of your
714
        project, so that anyone searching can quickly understand
715
        what this project is about.""")
716

    
717
app_comment_label    =  _("Comments for review (private)")
718
app_comment_help     =  _("""
719
        Write down any comments you may have for the reviewer
720
        of this application (e.g. background and rationale to
721
        support your request).
722
        The comments are strictly for the review process
723
        and will not be made public.""")
724

    
725
app_start_date_label =  _("Start date")
726
app_start_date_help  =  _("""
727
        Provide a date when your need your project to be created,
728
        and members to be able to join and get resources.
729
        This date is only a hint to help prioritize reviews.""")
730

    
731
app_end_date_label   =  _("Termination date")
732
app_end_date_help    =  _("""
733
        At this date, the project will be automatically terminated
734
        and its resource grants revoked from all members. If you are
735
        not certain, it is best to start with a conservative estimation.
736
        You can always re-apply for an extension, if you need.""")
737

    
738
join_policy_label    =  _("Joining policy")
739
app_member_join_policy_help    =  _("""
740
        Select how new members are accepted into the project.""")
741
leave_policy_label   =  _("Leaving policy")
742
app_member_leave_policy_help    =  _("""
743
        Select how new members can leave the project.""")
744

    
745
max_members_label    =  _("Maximum member count")
746
max_members_help     =  _("""
747
        Specify the maximum number of members this project may have,
748
        including the owner. Beyond this number, no new members
749
        may join the project and be granted the project resources.
750
        If you are not certain, it is best to start with a conservative
751
        limit. You can always request a raise when you need it.""")
752

    
753
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
754
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
755

    
756
class ProjectApplicationForm(forms.ModelForm):
757

    
758
    name = forms.CharField(
759
        label     = app_name_label,
760
        help_text = app_name_help,
761
        widget    = app_name_widget,
762
        validators = [app_name_validator])
763

    
764
    homepage = forms.URLField(
765
        label     = app_home_label,
766
        help_text = app_home_help,
767
        widget    = app_home_widget,
768
        required  = False)
769

    
770
    description = forms.CharField(
771
        label     = app_desc_label,
772
        help_text = app_desc_help,
773
        widget    = forms.Textarea,
774
        required  = False)
775

    
776
    comments = forms.CharField(
777
        label     = app_comment_label,
778
        help_text = app_comment_help,
779
        widget    = forms.Textarea,
780
        required  = False)
781

    
782
    start_date = forms.DateTimeField(
783
        label     = app_start_date_label,
784
        help_text = app_start_date_help,
785
        required  = False)
786

    
787
    end_date = forms.DateTimeField(
788
        label     = app_end_date_label,
789
        help_text = app_end_date_help)
790

    
791
    member_join_policy  = forms.TypedChoiceField(
792
        label     = join_policy_label,
793
        help_text = app_member_join_policy_help,
794
        initial   = 2,
795
        coerce    = int,
796
        choices   = join_policies)
797

    
798
    member_leave_policy = forms.TypedChoiceField(
799
        label     = leave_policy_label,
800
        help_text = app_member_leave_policy_help,
801
        coerce    = int,
802
        choices   = leave_policies)
803

    
804
    limit_on_members_number = forms.IntegerField(
805
        label     = max_members_label,
806
        help_text = max_members_help,
807
        min_value = 0,
808
        required  = False)
809

    
810
    class Meta:
811
        model = ProjectApplication
812
        fields = ( 'name', 'homepage', 'description',
813
                    'start_date', 'end_date', 'comments',
814
                    'member_join_policy', 'member_leave_policy',
815
                    'limit_on_members_number')
816

    
817
    def __init__(self, *args, **kwargs):
818
        instance = kwargs.get('instance')
819
        self.precursor_application = instance
820
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
821
        # in case of new application remove closed join policy
822
        if not instance:
823
            policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.copy()
824
            policies.pop(3)
825
            self.fields['member_join_policy'].choices = policies.iteritems()
826

    
827
    def clean_start_date(self):
828
        start_date = self.cleaned_data.get('start_date')
829
        if not self.precursor_application:
830
            today = datetime.now()
831
            today = datetime(today.year, today.month, today.day)
832
            if start_date and (start_date - today).days < 0:
833
                raise forms.ValidationError(
834
                _(astakos_messages.INVALID_PROJECT_START_DATE))
835
        return start_date
836

    
837
    def clean_end_date(self):
838
        start_date = self.cleaned_data.get('start_date')
839
        end_date = self.cleaned_data.get('end_date')
840
        today = datetime.now()
841
        today = datetime(today.year, today.month, today.day)
842
        if end_date and (end_date - today).days < 0:
843
            raise forms.ValidationError(
844
                _(astakos_messages.INVALID_PROJECT_END_DATE))
845
        if start_date and (end_date - start_date).days <= 0:
846
            raise forms.ValidationError(
847
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
848
        return end_date
849

    
850
    def clean(self):
851
        userid = self.data.get('user', None)
852
        policies = self.resource_policies
853
        self.user = None
854
        if userid:
855
            try:
856
                self.user = AstakosUser.objects.get(id=userid)
857
            except AstakosUser.DoesNotExist:
858
                pass
859
        if not self.user:
860
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
861
        super(ProjectApplicationForm, self).clean()
862
        return self.cleaned_data
863

    
864
    @property
865
    def resource_policies(self):
866
        policies = []
867
        append = policies.append
868
        for name, value in self.data.iteritems():
869
            if not value:
870
                continue
871
            uplimit = value
872
            if name.endswith('_uplimit'):
873
                subs = name.split('_uplimit')
874
                prefix, suffix = subs
875
                try:
876
                    resource = Resource.objects.get(name=prefix)
877
                except Resource.DoesNotExist:
878
                    raise forms.ValidationError("Resource %s does not exist" %
879
                                                resource.name)
880
                # keep only resource limits for selected resource groups
881
                if self.data.get(
882
                    'is_selected_%s' % resource.group, "0"
883
                 ) == "1":
884
                    if not resource.allow_in_projects:
885
                        raise forms.ValidationError("Invalid resource %s" %
886
                                                    resource.name)
887
                    d = model_to_dict(resource)
888
                    if uplimit:
889
                        d.update(dict(resource=prefix, uplimit=uplimit))
890
                    else:
891
                        d.update(dict(resource=prefix, uplimit=None))
892
                    append(d)
893

    
894
        ordered_keys = presentation.RESOURCES['resources_order']
895
        def resource_order(r):
896
            if r['str_repr'] in ordered_keys:
897
                return ordered_keys.index(r['str_repr'])
898
            else:
899
                return -1
900

    
901
        policies = sorted(policies, key=resource_order)
902
        return policies
903

    
904
    def cleaned_resource_policies(self):
905
        return [(d['name'], d['uplimit']) for d in self.resource_policies]
906

    
907
    def save(self, commit=True, **kwargs):
908
        data = dict(self.cleaned_data)
909
        data['precursor_id'] = self.instance.id
910
        is_new = self.instance.id is None
911
        data['owner'] = self.user if is_new else self.instance.owner
912
        data['resource_policies'] = self.cleaned_resource_policies()
913
        data['request_user'] = self.user
914
        submit_application(**data)
915

    
916

    
917
class ProjectSortForm(forms.Form):
918
    sorting = forms.ChoiceField(
919
        label='Sort by',
920
        choices=(('name', 'Sort by Name'),
921
                 ('issue_date', 'Sort by Issue date'),
922
                 ('start_date', 'Sort by Start Date'),
923
                 ('end_date', 'Sort by End Date'),
924
#                  ('approved_members_num', 'Sort by Participants'),
925
                 ('state', 'Sort by Status'),
926
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
927
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
928
                 ('-name', 'Sort by Name'),
929
                 ('-issue_date', 'Sort by Issue date'),
930
                 ('-start_date', 'Sort by Start Date'),
931
                 ('-end_date', 'Sort by End Date'),
932
#                  ('-approved_members_num', 'Sort by Participants'),
933
                 ('-state', 'Sort by Status'),
934
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
935
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
936
        ),
937
        required=True
938
    )
939

    
940
class AddProjectMembersForm(forms.Form):
941
    q = forms.CharField(
942
        widget=forms.Textarea(attrs={
943
            'placeholder': astakos_messages.ADD_PROJECT_MEMBERS_Q_PLACEHOLDER}
944
            ),
945
        label=_('Add members'),
946
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
947
        required=True,)
948

    
949
    def __init__(self, *args, **kwargs):
950
        chain_id = kwargs.pop('chain_id', None)
951
        if chain_id:
952
            self.project = Project.objects.get(id=chain_id)
953
        self.request_user = kwargs.pop('request_user', None)
954
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
955

    
956
    def clean(self):
957
        try:
958
            accept_membership_checks(self.project, self.request_user)
959
        except PermissionDenied, e:
960
            raise forms.ValidationError(e)
961

    
962
        q = self.cleaned_data.get('q') or ''
963
        users = q.split(',')
964
        users = list(u.strip() for u in users if u)
965
        db_entries = AstakosUser.objects.verified().filter(email__in=users)
966
        unknown = list(set(users) - set(u.email for u in db_entries))
967
        if unknown:
968
            raise forms.ValidationError(
969
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
970
        self.valid_users = db_entries
971
        return self.cleaned_data
972

    
973
    def get_valid_users(self):
974
        """Should be called after form cleaning"""
975
        try:
976
            return self.valid_users
977
        except:
978
            return ()
979

    
980
class ProjectMembersSortForm(forms.Form):
981
    sorting = forms.ChoiceField(
982
        label='Sort by',
983
        choices=(('person__email', 'User Id'),
984
                 ('person__first_name', 'Name'),
985
                 ('acceptance_date', 'Acceptance date')
986
        ),
987
        required=True
988
    )
989

    
990

    
991
class ProjectSearchForm(forms.Form):
992
    q = forms.CharField(max_length=200, label='Search project', required=False)
993

    
994

    
995
class ExtendedProfileForm(ProfileForm):
996
    """
997
    Profile form that combines `email change` and `password change` user
998
    actions by propagating submited data to internal EmailChangeForm
999
    and ExtendedPasswordChangeForm objects.
1000
    """
1001

    
1002
    password_change_form = None
1003
    email_change_form = None
1004

    
1005
    password_change = False
1006
    email_change = False
1007

    
1008
    extra_forms_fields = {
1009
        'email': ['new_email_address'],
1010
        'password': ['old_password', 'new_password1', 'new_password2']
1011
    }
1012

    
1013
    fields = ('email')
1014
    change_password = forms.BooleanField(initial=False, required=False)
1015
    change_email = forms.BooleanField(initial=False, required=False)
1016

    
1017
    email_changed = False
1018
    password_changed = False
1019

    
1020
    def __init__(self, *args, **kwargs):
1021
        session_key = kwargs.get('session_key', None)
1022
        self.fields_list = [
1023
                'email',
1024
                'new_email_address',
1025
                'first_name',
1026
                'last_name',
1027
                'old_password',
1028
                'new_password1',
1029
                'new_password2',
1030
                'change_email',
1031
                'change_password',
1032
        ]
1033

    
1034
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
1035
        self.session_key = session_key
1036
        if self.instance.can_change_password():
1037
            self.password_change = True
1038
        else:
1039
            self.fields_list.remove('old_password')
1040
            self.fields_list.remove('new_password1')
1041
            self.fields_list.remove('new_password2')
1042
            self.fields_list.remove('change_password')
1043
            del self.fields['change_password']
1044

    
1045
        if settings.EMAILCHANGE_ENABLED and self.instance.can_change_email():
1046
            self.email_change = True
1047
        else:
1048
            self.fields_list.remove('new_email_address')
1049
            self.fields_list.remove('change_email')
1050
            del self.fields['change_email']
1051

    
1052
        self._init_extra_forms()
1053
        self.save_extra_forms = []
1054
        self.success_messages = []
1055
        self.fields.keyOrder = self.fields_list
1056

    
1057

    
1058
    def _init_extra_form_fields(self):
1059
        if self.email_change:
1060
            self.fields.update(self.email_change_form.fields)
1061
            self.fields['new_email_address'].required = False
1062
            self.fields['email'].help_text = _('Change the email associated with '
1063
                                               'your account. This email will '
1064
                                               'remain active until you verify '
1065
                                               'your new one.')
1066

    
1067
        if self.password_change:
1068
            self.fields.update(self.password_change_form.fields)
1069
            self.fields['old_password'].required = False
1070
            self.fields['old_password'].label = _('Password')
1071
            self.fields['old_password'].help_text = _('Change your password.')
1072
            self.fields['old_password'].initial = 'password'
1073
            self.fields['new_password1'].required = False
1074
            self.fields['new_password2'].required = False
1075

    
1076
    def _update_extra_form_errors(self):
1077
        if self.cleaned_data.get('change_password'):
1078
            self.errors.update(self.password_change_form.errors)
1079
        if self.cleaned_data.get('change_email'):
1080
            self.errors.update(self.email_change_form.errors)
1081

    
1082
    def _init_extra_forms(self):
1083
        self.email_change_form = EmailChangeForm(self.data)
1084
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1085
                                   data=self.data, session_key=self.session_key)
1086
        self._init_extra_form_fields()
1087

    
1088
    def is_valid(self):
1089
        password, email = True, True
1090
        profile = super(ExtendedProfileForm, self).is_valid()
1091
        if profile and self.cleaned_data.get('change_password', None):
1092
            self.password_change_form.fields['new_password1'].required = True
1093
            self.password_change_form.fields['new_password2'].required = True
1094
            password = self.password_change_form.is_valid()
1095
            self.save_extra_forms.append('password')
1096
        if profile and self.cleaned_data.get('change_email'):
1097
            self.fields['new_email_address'].required = True
1098
            email = self.email_change_form.is_valid()
1099
            self.save_extra_forms.append('email')
1100

    
1101
        if not password or not email:
1102
            self._update_extra_form_errors()
1103

    
1104
        return all([profile, password, email])
1105

    
1106
    def save(self, request, *args, **kwargs):
1107
        if 'email' in self.save_extra_forms:
1108
            self.email_change_form.save(request, *args, **kwargs)
1109
            self.email_changed = True
1110
        if 'password' in self.save_extra_forms:
1111
            self.password_change_form.save(*args, **kwargs)
1112
            self.password_changed = True
1113
        return super(ExtendedProfileForm, self).save(*args, **kwargs)