Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 69b26576

History | View | Annotate | Download (33.1 kB)

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

    
37
from django import forms
38
from django.utils.translation import ugettext as _
39
from django.contrib.auth.forms import (
40
    UserCreationForm, AuthenticationForm,
41
    PasswordResetForm, PasswordChangeForm,
42
    SetPasswordForm)
43
from django.core.mail import send_mail
44
from django.contrib.auth.tokens import default_token_generator
45
from django.template import Context, loader
46
from django.utils.http import int_to_base36
47
from django.core.urlresolvers import reverse
48
from django.utils.safestring import mark_safe
49
from django.utils.encoding import smart_str
50
from django.conf import settings
51
from django.forms.models import fields_for_model
52
from django.db import transaction
53
from django.utils.encoding import smart_unicode
54
from django.core import validators
55
from django.contrib.auth.models import AnonymousUser
56
from django.core.exceptions import PermissionDenied
57

    
58
from astakos.im.models import (
59
    AstakosUser, EmailChange, Invitation,
60
    Resource, PendingThirdPartyUser, get_latest_terms, RESOURCE_SEPARATOR,
61
    ProjectApplication, Project)
62
from astakos.im.settings import (
63
    INVITATIONS_PER_LEVEL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY,
64
    RECAPTCHA_ENABLED, DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
65
    PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN,
66
    MODERATION_ENABLED, PROJECT_MEMBER_JOIN_POLICIES,
67
    PROJECT_MEMBER_LEAVE_POLICIES)
68
from astakos.im.widgets import DummyWidget, RecaptchaWidget
69
from astakos.im.functions import (
70
    send_change_email, submit_application, do_accept_membership_checks)
71

    
72
from astakos.im.util import reserved_email, get_query, model_to_dict
73
from astakos.im import auth_providers
74

    
75
import astakos.im.messages as astakos_messages
76

    
77
import logging
78
import hashlib
79
import recaptcha.client.captcha as captcha
80
import re
81

    
82
logger = logging.getLogger(__name__)
83

    
84
DOMAIN_VALUE_REGEX = re.compile(
85
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
86
    re.IGNORECASE)
87

    
88
class StoreUserMixin(object):
89

    
90
    @transaction.commit_on_success
91
    def store_user(self, user, request):
92
        user.save()
93
        self.post_store_user(user, request)
94
        return user
95

    
96
    def post_store_user(self, user, request):
97
        """
98
        Interface method for descendant backends to be able to do stuff within
99
        the transaction enabled by store_user.
100
        """
101
        pass
102

    
103

    
104
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
105
    """
106
    Extends the built in UserCreationForm in several ways:
107

108
    * Adds email, first_name, last_name, recaptcha_challenge_field,
109
    * recaptcha_response_field field.
110
    * The username field isn't visible and it is assigned a generated id.
111
    * User created is not active.
112
    """
113
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
114
    recaptcha_response_field = forms.CharField(
115
        widget=RecaptchaWidget, label='')
116

    
117
    class Meta:
118
        model = AstakosUser
119
        fields = ("email", "first_name", "last_name",
120
                  "has_signed_terms", "has_signed_terms")
121

    
122
    def __init__(self, *args, **kwargs):
123
        """
124
        Changes the order of fields, and removes the username field.
125
        """
126
        request = kwargs.pop('request', None)
127
        if request:
128
            self.ip = request.META.get('REMOTE_ADDR',
129
                                       request.META.get('HTTP_X_REAL_IP', None))
130

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

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

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

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

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

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

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

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

    
180
    def post_store_user(self, user, request):
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):
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)
194
        user.renew_token()
195
        if commit:
196
            user.save()
197
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
198
        return user
199

    
200

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

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

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

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

    
228

    
229
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
230
    id = forms.CharField(
231
        widget=forms.HiddenInput(),
232
        label='',
233
        required=False
234
    )
235
    third_party_identifier = forms.CharField(
236
        widget=forms.HiddenInput(),
237
        label=''
238
    )
239
    class Meta:
240
        model = AstakosUser
241
        fields = ['id', 'email', 'third_party_identifier', 'first_name', 'last_name']
242

    
243
    def __init__(self, *args, **kwargs):
244
        """
245
        Changes the order of fields, and removes the username field.
246
        """
247
        self.request = kwargs.get('request', None)
248
        if self.request:
249
            kwargs.pop('request')
250

    
251
        latest_terms = get_latest_terms()
252
        if latest_terms:
253
            self._meta.fields.append('has_signed_terms')
254

    
255
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
256

    
257
        if latest_terms:
258
            self.fields.keyOrder.append('has_signed_terms')
259

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

    
268
    def clean_email(self):
269
        email = self.cleaned_data['email']
270
        if not email:
271
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
272
        if reserved_email(email):
273
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
274
        return email
275

    
276
    def clean_has_signed_terms(self):
277
        has_signed_terms = self.cleaned_data['has_signed_terms']
278
        if not has_signed_terms:
279
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
280
        return has_signed_terms
281

    
282
    def post_store_user(self, user, request):
283
        pending = PendingThirdPartyUser.objects.get(
284
                                token=request.POST.get('third_party_token'),
285
                                third_party_identifier= \
286
            self.cleaned_data.get('third_party_identifier'))
287
        return user.add_pending_auth_provider(pending)
288

    
289

    
290
    def save(self, commit=True):
291
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
292
        user.set_unusable_password()
293
        user.renew_token()
294
        if commit:
295
            user.save()
296
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
297
        return user
298

    
299

    
300
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
301
    """
302
    Extends the ThirdPartyUserCreationForm: email is readonly.
303
    """
304
    def __init__(self, *args, **kwargs):
305
        """
306
        Changes the order of fields, and removes the username field.
307
        """
308
        super(
309
            InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
310

    
311
        #set readonly form fields
312
        ro = ('email',)
313
        for f in ro:
314
            self.fields[f].widget.attrs['readonly'] = True
315

    
316
    def save(self, commit=True):
317
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
318
        user.set_invitation_level()
319
        user.email_verified = True
320
        if commit:
321
            user.save()
322
        return user
323

    
324

    
325
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
326
    additional_email = forms.CharField(
327
        widget=forms.HiddenInput(), label='', required=False)
328

    
329
    def __init__(self, *args, **kwargs):
330
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
331
        # copy email value to additional_mail in case user will change it
332
        name = 'email'
333
        field = self.fields[name]
334
        self.initial['additional_email'] = self.initial.get(name, field.initial)
335
        self.initial['email'] = None
336

    
337

    
338
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
339
                                        InvitedThirdPartyUserCreationForm):
340
    pass
341

    
342

    
343
class LoginForm(AuthenticationForm):
344
    username = forms.EmailField(label=_("Email"))
345
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
346
    recaptcha_response_field = forms.CharField(
347
        widget=RecaptchaWidget, label='')
348

    
349
    def __init__(self, *args, **kwargs):
350
        was_limited = kwargs.get('was_limited', False)
351
        request = kwargs.get('request', None)
352
        if request:
353
            self.ip = request.META.get('REMOTE_ADDR',
354
                                       request.META.get('HTTP_X_REAL_IP', None))
355

    
356
        t = ('request', 'was_limited')
357
        for elem in t:
358
            if elem in kwargs.keys():
359
                kwargs.pop(elem)
360
        super(LoginForm, self).__init__(*args, **kwargs)
361

    
362
        self.fields.keyOrder = ['username', 'password']
363
        if was_limited and RECAPTCHA_ENABLED:
364
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
365
                                         'recaptcha_response_field', ])
366

    
367
    def clean_username(self):
368
        return self.cleaned_data['username'].lower()
369

    
370
    def clean_recaptcha_response_field(self):
371
        if 'recaptcha_challenge_field' in self.cleaned_data:
372
            self.validate_captcha()
373
        return self.cleaned_data['recaptcha_response_field']
374

    
375
    def clean_recaptcha_challenge_field(self):
376
        if 'recaptcha_response_field' in self.cleaned_data:
377
            self.validate_captcha()
378
        return self.cleaned_data['recaptcha_challenge_field']
379

    
380
    def validate_captcha(self):
381
        rcf = self.cleaned_data['recaptcha_challenge_field']
382
        rrf = self.cleaned_data['recaptcha_response_field']
383
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
384
        if not check.is_valid:
385
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
386

    
387
    def clean(self):
388
        """
389
        Override default behavior in order to check user's activation later
390
        """
391
        username = self.cleaned_data.get('username')
392

    
393
        if username:
394
            try:
395
                user = AstakosUser.objects.get_by_identifier(username)
396
                if not user.has_auth_provider('local'):
397
                    provider = auth_providers.get_provider('local')
398
                    raise forms.ValidationError(
399
                        _(provider.get_message('NOT_ACTIVE_FOR_USER')))
400
            except AstakosUser.DoesNotExist:
401
                pass
402

    
403
        try:
404
            super(LoginForm, self).clean()
405
        except forms.ValidationError, e:
406
            if self.user_cache is None:
407
                raise
408
            if not self.user_cache.is_active:
409
                raise forms.ValidationError(self.user_cache.get_inactive_message())
410
            if self.request:
411
                if not self.request.session.test_cookie_worked():
412
                    raise
413
        return self.cleaned_data
414

    
415

    
416
class ProfileForm(forms.ModelForm):
417
    """
418
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
419
    Most of the fields are readonly since the user is not allowed to change
420
    them.
421

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

    
427
    class Meta:
428
        model = AstakosUser
429
        fields = ('email', 'first_name', 'last_name', 'auth_token',
430
                  'auth_token_expires')
431

    
432
    def __init__(self, *args, **kwargs):
433
        self.session_key = kwargs.pop('session_key', None)
434
        super(ProfileForm, self).__init__(*args, **kwargs)
435
        instance = getattr(self, 'instance', None)
436
        ro_fields = ('email', 'auth_token', 'auth_token_expires')
437
        if instance and instance.id:
438
            for field in ro_fields:
439
                self.fields[field].widget.attrs['readonly'] = True
440

    
441
    def save(self, commit=True):
442
        user = super(ProfileForm, self).save(commit=False)
443
        user.is_verified = True
444
        if self.cleaned_data.get('renew'):
445
            user.renew_token(
446
                flush_sessions=True,
447
                current_key=self.session_key
448
            )
449
        if commit:
450
            user.save()
451
        return user
452

    
453

    
454
class FeedbackForm(forms.Form):
455
    """
456
    Form for writing feedback.
457
    """
458
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
459
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
460
                                    required=False)
461

    
462

    
463
class SendInvitationForm(forms.Form):
464
    """
465
    Form for sending an invitations
466
    """
467

    
468
    email = forms.EmailField(required=True, label='Email address')
469
    first_name = forms.EmailField(label='First name')
470
    last_name = forms.EmailField(label='Last name')
471

    
472

    
473
class ExtendedPasswordResetForm(PasswordResetForm):
474
    """
475
    Extends PasswordResetForm by overriding
476

477
    save method: to pass a custom from_email in send_mail.
478
    clean_email: to handle local auth provider checks
479
    """
480
    def clean_email(self):
481
        email = super(ExtendedPasswordResetForm, self).clean_email()
482
        try:
483
            user = AstakosUser.objects.get_by_identifier(email)
484

    
485
            if not user.is_active:
486
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
487

    
488
            if not user.has_usable_password():
489
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
490

    
491
            if not user.can_change_password():
492
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
493
        except AstakosUser.DoesNotExist, e:
494
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
495
        return email
496

    
497
    def save(
498
        self, domain_override=None, email_template_name='registration/password_reset_email.html',
499
            use_https=False, token_generator=default_token_generator, request=None):
500
        """
501
        Generates a one-use only link for resetting password and sends to the user.
502
        """
503
        for user in self.users_cache:
504
            url = user.astakosuser.get_password_reset_url(token_generator)
505
            url = urljoin(BASEURL, url)
506
            t = loader.get_template(email_template_name)
507
            c = {
508
                'email': user.email,
509
                'url': url,
510
                'site_name': SITENAME,
511
                'user': user,
512
                'baseurl': BASEURL,
513
                'support': DEFAULT_CONTACT_EMAIL
514
            }
515
            from_email = settings.SERVER_EMAIL
516
            send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
517
                      t.render(Context(c)), from_email, [user.email])
518

    
519

    
520
class EmailChangeForm(forms.ModelForm):
521

    
522
    class Meta:
523
        model = EmailChange
524
        fields = ('new_email_address',)
525

    
526
    def clean_new_email_address(self):
527
        addr = self.cleaned_data['new_email_address']
528
        if AstakosUser.objects.filter(email__iexact=addr):
529
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
530
        return addr
531

    
532
    def save(self, email_template_name, request, commit=True):
533
        ec = super(EmailChangeForm, self).save(commit=False)
534
        ec.user = request.user
535
        activation_key = hashlib.sha1(
536
            str(random()) + smart_str(ec.new_email_address))
537
        ec.activation_key = activation_key.hexdigest()
538
        if commit:
539
            ec.save()
540
        send_change_email(ec, request, email_template_name=email_template_name)
541

    
542

    
543
class SignApprovalTermsForm(forms.ModelForm):
544

    
545
    class Meta:
546
        model = AstakosUser
547
        fields = ("has_signed_terms",)
548

    
549
    def __init__(self, *args, **kwargs):
550
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
551

    
552
    def clean_has_signed_terms(self):
553
        has_signed_terms = self.cleaned_data['has_signed_terms']
554
        if not has_signed_terms:
555
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
556
        return has_signed_terms
557

    
558

    
559
class InvitationForm(forms.ModelForm):
560

    
561
    username = forms.EmailField(label=_("Email"))
562

    
563
    def __init__(self, *args, **kwargs):
564
        super(InvitationForm, self).__init__(*args, **kwargs)
565

    
566
    class Meta:
567
        model = Invitation
568
        fields = ('username', 'realname')
569

    
570
    def clean_username(self):
571
        username = self.cleaned_data['username']
572
        try:
573
            Invitation.objects.get(username=username)
574
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
575
        except Invitation.DoesNotExist:
576
            pass
577
        return username
578

    
579

    
580
class ExtendedPasswordChangeForm(PasswordChangeForm):
581
    """
582
    Extends PasswordChangeForm by enabling user
583
    to optionally renew also the token.
584
    """
585
    if not NEWPASSWD_INVALIDATE_TOKEN:
586
        renew = forms.BooleanField(label='Renew token', required=False,
587
                                   initial=True,
588
                                   help_text='Unsetting this may result in security risk.')
589

    
590
    def __init__(self, user, *args, **kwargs):
591
        self.session_key = kwargs.pop('session_key', None)
592
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
593

    
594
    def save(self, commit=True):
595
        try:
596
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
597
                self.user.renew_token()
598
            self.user.flush_sessions(current_key=self.session_key)
599
        except AttributeError:
600
            # if user model does has not such methods
601
            pass
602
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
603

    
604
class ExtendedSetPasswordForm(SetPasswordForm):
605
    """
606
    Extends SetPasswordForm by enabling user
607
    to optionally renew also the token.
608
    """
609
    if not NEWPASSWD_INVALIDATE_TOKEN:
610
        renew = forms.BooleanField(
611
            label='Renew token',
612
            required=False,
613
            initial=True,
614
            help_text='Unsetting this may result in security risk.')
615

    
616
    def __init__(self, user, *args, **kwargs):
617
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
618

    
619
    @transaction.commit_on_success()
620
    def save(self, commit=True):
621
        try:
622
            self.user = AstakosUser.objects.get(id=self.user.id)
623
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
624
                self.user.renew_token()
625
            #self.user.flush_sessions()
626
            if not self.user.has_auth_provider('local'):
627
                self.user.add_auth_provider('local', auth_backend='astakos')
628

    
629
        except BaseException, e:
630
            logger.exception(e)
631
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
632

    
633

    
634

    
635

    
636
app_name_label       =  "Project name"
637
app_name_placeholder = _("myproject.mylab.ntua.gr")
638
app_name_validator   =  validators.RegexValidator(
639
                            DOMAIN_VALUE_REGEX,
640
                            _(astakos_messages.DOMAIN_VALUE_ERR),
641
                            'invalid')
642
app_name_help        =  _("""
643
        The Project's name should be in a domain format.
644
        The domain shouldn't neccessarily exist in the real
645
        world but is helpful to imply a structure.
646
        e.g.: myproject.mylab.ntua.gr or
647
        myservice.myteam.myorganization""")
648
app_name_widget      =  forms.TextInput(
649
                            attrs={'placeholder': app_name_placeholder})
650

    
651

    
652
app_home_label       =  "Homepage URL"
653
app_home_placeholder =  'myinstitution.org/myproject/'
654
app_home_help        =  _("""
655
        URL pointing at your project's site.
656
        e.g.: myinstitution.org/myproject/.
657
        Leave blank if there is no website.""")
658
app_home_widget      =  forms.TextInput(
659
                            attrs={'placeholder': app_home_placeholder})
660

    
661
app_desc_label       =  _("Description")
662
app_desc_help        =  _("""
663
        Please provide a short but descriptive abstract of your Project,
664
        so that anyone searching can quickly understand
665
        what this Project is about.""")
666

    
667
app_comment_label    =  _("Comments for review (private)")
668
app_comment_help     =  _("""
669
        Write down any comments you may have for the reviewer
670
        of this application (e.g. background and rationale to
671
        support your request).
672
        The comments are strictly for the review process
673
        and will not be published.""")
674

    
675
app_start_date_label =  _("Start date")
676
app_start_date_help  =  _("""
677
        Provide a date when your need your project to be created,
678
        and members to be able to join and get resources.
679
        This date is only a hint to help prioritize reviews.""")
680

    
681
app_end_date_label   =  _("Termination date")
682
app_end_date_help    =  _("""
683
        At this date, the project will be automatically terminated
684
        and its resource grants revoked from all members.
685
        Unless you know otherwise,
686
        it is best to start with a conservative estimation.
687
        You can always re-apply for an extension, if you need.""")
688

    
689
join_policy_label    =  _("Joining policy")
690
leave_policy_label   =  _("Leaving policy")
691

    
692
max_members_label    =  _("Maximum member count")
693
max_members_help     =  _("""
694
        Specify the maximum number of members this project may have,
695
        including the owner. Beyond this number, no new members
696
        may join the project and be granted the project resources.
697
        Unless you certainly for otherwise,
698
        it is best to start with a conservative limit.
699
        You can always request a raise when you need it.""")
700

    
701
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
702
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
703

    
704
class ProjectApplicationForm(forms.ModelForm):
705

    
706
    name = forms.CharField(
707
        label     = app_name_label,
708
        help_text = app_name_help,
709
        widget    = app_name_widget,
710
        validators = [app_name_validator])
711

    
712
    homepage = forms.URLField(
713
        label     = app_home_label,
714
        help_text = app_home_help,
715
        widget    = app_home_widget,
716
        required  = False)
717

    
718
    description = forms.CharField(
719
        label     = app_desc_label,
720
        help_text = app_desc_help,
721
        widget    = forms.Textarea,
722
        required  = False)
723

    
724
    comments = forms.CharField(
725
        label     = app_comment_label,
726
        help_text = app_comment_help,
727
        widget    = forms.Textarea,
728
        required  = False)
729

    
730
    start_date = forms.DateTimeField(
731
        label     = app_start_date_label,
732
        help_text = app_start_date_help,
733
        required  = False)
734

    
735
    end_date = forms.DateTimeField(
736
        label     = app_end_date_label,
737
        help_text = app_end_date_help)
738

    
739
    member_join_policy  = forms.TypedChoiceField(
740
        label     = join_policy_label,
741
        initial   = 2,
742
        coerce    = int,
743
        choices   = join_policies)
744

    
745
    member_leave_policy = forms.TypedChoiceField(
746
        label     = leave_policy_label,
747
        coerce    = int,
748
        choices   = leave_policies)
749

    
750
    limit_on_members_number = forms.IntegerField(
751
        label     = max_members_label,
752
        help_text = max_members_help,
753
        required  = False)
754

    
755
    class Meta:
756
        model = ProjectApplication
757
        #include = ( 'name', 'homepage', 'description',
758
        #            'start_date', 'end_date', 'comments')
759

    
760
        fields = ( 'name', 'homepage', 'description',
761
                    'start_date', 'end_date', 'comments',
762
                    'member_join_policy', 'member_leave_policy',
763
                    'limit_on_members_number')
764

    
765
    def __init__(self, *args, **kwargs):
766
        self.precursor_application = kwargs.get('instance')
767
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
768

    
769
    def clean_start_date(self):
770
        start_date = self.cleaned_data.get('start_date')
771
        now = datetime.now()
772
        if start_date and now - start_date > timedelta(days=1):
773
            raise forms.ValidationError(
774
                _(astakos_messages.INVALID_PROJECT_START_DATE))
775
        return start_date
776

    
777
    def clean_end_date(self):
778
        start_date = self.cleaned_data.get('start_date')
779
        end_date = self.cleaned_data.get('end_date')
780
        now = datetime.now()
781
        if end_date and now - end_date > timedelta(days=1):
782
            raise forms.ValidationError(
783
                _(astakos_messages.INVALID_PROJECT_END_DATE))
784
        if start_date and end_date <= start_date:
785
            raise forms.ValidationError(
786
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
787
        return end_date
788

    
789
    def clean(self):
790
        userid = self.data.get('user', None)
791
        self.user = None
792
        if userid:
793
            try:
794
                self.user = AstakosUser.objects.get(id=userid)
795
            except AstakosUser.DoesNotExist:
796
                pass
797
        if not self.user:
798
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
799
        super(ProjectApplicationForm, self).clean()
800
        return self.cleaned_data
801

    
802
    @property
803
    def resource_policies(self):
804
        policies = []
805
        append = policies.append
806
        for name, value in self.data.iteritems():
807
            if not value:
808
                continue
809
            uplimit = value
810
            if name.endswith('_uplimit'):
811
                subs = name.split('_uplimit')
812
                prefix, suffix = subs
813
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
814
                resource = Resource.objects.get(service__name=s, name=r)
815

    
816
                # keep only resource limits for selected resource groups
817
                if self.data.get(
818
                    'is_selected_%s' % resource.group, "0"
819
                 ) == "1":
820
                    d = model_to_dict(resource)
821
                    if uplimit:
822
                        d.update(dict(service=s, resource=r, uplimit=uplimit))
823
                    else:
824
                        d.update(dict(service=s, resource=r, uplimit=None))
825
                    append(d)
826

    
827
        return policies
828

    
829

    
830
    def save(self, commit=True):
831
        application = super(ProjectApplicationForm, self).save(commit=False)
832
        applicant = self.user
833
        comments = self.cleaned_data.pop('comments', None)
834
        return submit_application(
835
            application,
836
            self.resource_policies,
837
            applicant,
838
            comments,
839
            self.precursor_application
840
        )
841

    
842
class ProjectSortForm(forms.Form):
843
    sorting = forms.ChoiceField(
844
        label='Sort by',
845
        choices=(('name', 'Sort by Name'),
846
                 ('issue_date', 'Sort by Issue date'),
847
                 ('start_date', 'Sort by Start Date'),
848
                 ('end_date', 'Sort by End Date'),
849
#                  ('approved_members_num', 'Sort by Participants'),
850
                 ('state', 'Sort by Status'),
851
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
852
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
853
                 ('-name', 'Sort by Name'),
854
                 ('-issue_date', 'Sort by Issue date'),
855
                 ('-start_date', 'Sort by Start Date'),
856
                 ('-end_date', 'Sort by End Date'),
857
#                  ('-approved_members_num', 'Sort by Participants'),
858
                 ('-state', 'Sort by Status'),
859
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
860
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
861
        ),
862
        required=True
863
    )
864

    
865
class AddProjectMembersForm(forms.Form):
866
    q = forms.CharField(
867
        max_length=800, widget=forms.Textarea, label=_('Add members'),
868
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
869

    
870
    def __init__(self, *args, **kwargs):
871
        application_id = kwargs.pop('application_id', None)
872
        if application_id:
873
            self.project = Project.objects.get(application__id=application_id)
874
        self.request_user = kwargs.pop('request_user', None)
875
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
876
        
877
    def clean(self):
878
        try:
879
            do_accept_membership_checks(self.project, self.request_user)
880
        except PermissionDenied, e:
881
            raise forms.ValidationError(e)
882

    
883
        q = self.cleaned_data.get('q') or ''
884
        users = q.split(',')
885
        users = list(u.strip() for u in users if u)
886
        db_entries = AstakosUser.objects.filter(email__in=users)
887
        unknown = list(set(users) - set(u.email for u in db_entries))
888
        if unknown:
889
            raise forms.ValidationError(
890
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
891
        self.valid_users = db_entries
892
        return self.cleaned_data
893

    
894
    def get_valid_users(self):
895
        """Should be called after form cleaning"""
896
        try:
897
            return self.valid_users
898
        except:
899
            return ()
900

    
901
class ProjectMembersSortForm(forms.Form):
902
    sorting = forms.ChoiceField(
903
        label='Sort by',
904
        choices=(('person__email', 'User Id'),
905
                 ('person__first_name', 'Name'),
906
                 ('acceptance_date', 'Acceptance date')
907
        ),
908
        required=True
909
    )
910

    
911
class ProjectSearchForm(forms.Form):
912
    q = forms.CharField(max_length=200, label='Search project', required=False)
913