Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ e47fb17a

History | View | Annotate | Download (32.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 urlparse import urljoin
34
from random import random
35

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

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

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

    
74
import astakos.im.messages as astakos_messages
75

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

    
81
logger = logging.getLogger(__name__)
82

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

    
87
class StoreUserMixin(object):
88

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

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

    
102

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

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

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

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

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

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

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

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

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

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

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

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

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

    
187
    def save(self, commit=True):
188
        """
189
        Saves the email, first_name and last_name properties, after the normal
190
        save behavior is complete.
191
        """
192
        user = super(LocalUserCreationForm, self).save(commit=False)
193
        user.renew_token()
194
        if commit:
195
            user.save()
196
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
197
        return user
198

    
199

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

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

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

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

    
227

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

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

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

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

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

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

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

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

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

    
288

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

    
298

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

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

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

    
323

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

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

    
336

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

    
341

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

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

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

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

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

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

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

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

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

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

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

    
414

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

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

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

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

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

    
452

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

    
461

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

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

    
471

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

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

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

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

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

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

    
518

    
519
class EmailChangeForm(forms.ModelForm):
520

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

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

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

    
541

    
542
class SignApprovalTermsForm(forms.ModelForm):
543

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

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

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

    
557

    
558
class InvitationForm(forms.ModelForm):
559

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

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

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

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

    
578

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

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

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

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

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

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

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

    
632

    
633

    
634

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

    
650

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

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

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

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

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

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

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

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

    
703
class ProjectApplicationForm(forms.ModelForm):
704

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

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

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

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

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

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

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

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

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

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

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

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

    
768
    def clean(self):
769
        userid = self.data.get('user', None)
770
        self.user = None
771
        if userid:
772
            try:
773
                self.user = AstakosUser.objects.get(id=userid)
774
            except AstakosUser.DoesNotExist:
775
                pass
776
        if not self.user:
777
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
778
        super(ProjectApplicationForm, self).clean()
779
        return self.cleaned_data
780

    
781
    @property
782
    def resource_policies(self):
783
        policies = []
784
        append = policies.append
785
        for name, value in self.data.iteritems():
786
            if not value:
787
                continue
788
            uplimit = value
789
            if name.endswith('_uplimit'):
790
                subs = name.split('_uplimit')
791
                prefix, suffix = subs
792
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
793
                resource = Resource.objects.get(service__name=s, name=r)
794

    
795
                # keep only resource limits for selected resource groups
796
                if self.data.get(
797
                    'is_selected_%s' % resource.group, "0"
798
                 ) == "1":
799
                    d = model_to_dict(resource)
800
                    if uplimit:
801
                        d.update(dict(service=s, resource=r, uplimit=uplimit))
802
                    else:
803
                        d.update(dict(service=s, resource=r, uplimit=None))
804
                    append(d)
805

    
806
        return policies
807

    
808

    
809
    def save(self, commit=True):
810
        application = super(ProjectApplicationForm, self).save(commit=False)
811
        applicant = self.user
812
        comments = self.cleaned_data.pop('comments', None)
813
        return submit_application(
814
            application,
815
            self.resource_policies,
816
            applicant,
817
            comments,
818
            self.precursor_application
819
        )
820

    
821
class ProjectSortForm(forms.Form):
822
    sorting = forms.ChoiceField(
823
        label='Sort by',
824
        choices=(('name', 'Sort by Name'),
825
                 ('issue_date', 'Sort by Issue date'),
826
                 ('start_date', 'Sort by Start Date'),
827
                 ('end_date', 'Sort by End Date'),
828
#                  ('approved_members_num', 'Sort by Participants'),
829
                 ('state', 'Sort by Status'),
830
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
831
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
832
                 ('-name', 'Sort by Name'),
833
                 ('-issue_date', 'Sort by Issue date'),
834
                 ('-start_date', 'Sort by Start Date'),
835
                 ('-end_date', 'Sort by End Date'),
836
#                  ('-approved_members_num', 'Sort by Participants'),
837
                 ('-state', 'Sort by Status'),
838
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
839
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
840
        ),
841
        required=True
842
    )
843

    
844
class AddProjectMembersForm(forms.Form):
845
    q = forms.CharField(
846
        max_length=800, widget=forms.Textarea, label=_('Add members'),
847
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
848

    
849
    def __init__(self, *args, **kwargs):
850
        application_id = kwargs.pop('application_id', None)
851
        if application_id:
852
            self.project = Project.objects.get(application__id=application_id)
853
        self.request_user = kwargs.pop('request_user', None)
854
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
855
        
856
    def clean(self):
857
        try:
858
            do_accept_membership_checks(self.project, self.request_user)
859
        except PermissionDenied, e:
860
            raise forms.ValidationError(e)
861

    
862
        q = self.cleaned_data.get('q') or ''
863
        users = q.split(',')
864
        users = list(u.strip() for u in users if u)
865
        db_entries = AstakosUser.objects.filter(email__in=users)
866
        unknown = list(set(users) - set(u.email for u in db_entries))
867
        if unknown:
868
            raise forms.ValidationError(
869
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
870
        self.valid_users = db_entries
871
        return self.cleaned_data
872

    
873
    def get_valid_users(self):
874
        """Should be called after form cleaning"""
875
        try:
876
            return self.valid_users
877
        except:
878
            return ()
879

    
880
class ProjectMembersSortForm(forms.Form):
881
    sorting = forms.ChoiceField(
882
        label='Sort by',
883
        choices=(('person__email', 'User Id'),
884
                 ('person__first_name', 'Name'),
885
                 ('acceptance_date', 'Acceptance date')
886
        ),
887
        required=True
888
    )
889

    
890
class ProjectSearchForm(forms.Form):
891
    q = forms.CharField(max_length=200, label='Search project', required=False)
892