Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33.5 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, reserved_verified_email, \
73
                            get_query, model_to_dict
74
from astakos.im import auth_providers
75

    
76
import astakos.im.messages as astakos_messages
77

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

    
83
logger = logging.getLogger(__name__)
84

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

    
89
today = datetime.now()
90
today = datetime(today.year, today.month, today.day)
91

    
92
class StoreUserMixin(object):
93

    
94
    def store_user(self, user, request):
95
        """
96
        WARNING: this should be wrapped inside a transactional view/method.
97
        """
98
        user.save()
99
        self.post_store_user(user, request)
100
        return user
101

    
102
    def post_store_user(self, user, request):
103
        """
104
        Interface method for descendant backends to be able to do stuff within
105
        the transaction enabled by store_user.
106
        """
107
        pass
108

    
109

    
110
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
111
    """
112
    Extends the built in UserCreationForm in several ways:
113

114
    * Adds email, first_name, last_name, recaptcha_challenge_field,
115
    * recaptcha_response_field field.
116
    * The username field isn't visible and it is assigned a generated id.
117
    * User created is not active.
118
    """
119
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
120
    recaptcha_response_field = forms.CharField(
121
        widget=RecaptchaWidget, label='')
122

    
123
    class Meta:
124
        model = AstakosUser
125
        fields = ("email", "first_name", "last_name",
126
                  "has_signed_terms", "has_signed_terms")
127

    
128
    def __init__(self, *args, **kwargs):
129
        """
130
        Changes the order of fields, and removes the username field.
131
        """
132
        request = kwargs.pop('request', None)
133
        if request:
134
            self.ip = request.META.get('REMOTE_ADDR',
135
                                       request.META.get('HTTP_X_REAL_IP', None))
136

    
137
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
138
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
139
                                'password1', 'password2']
140

    
141
        if RECAPTCHA_ENABLED:
142
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
143
                                         'recaptcha_response_field', ])
144
        if get_latest_terms():
145
            self.fields.keyOrder.append('has_signed_terms')
146

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

    
155
    def clean_email(self):
156
        email = self.cleaned_data['email']
157
        if not email:
158
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
159
        if reserved_verified_email(email):
160
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
161
        return email
162

    
163
    def clean_has_signed_terms(self):
164
        has_signed_terms = self.cleaned_data['has_signed_terms']
165
        if not has_signed_terms:
166
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
167
        return has_signed_terms
168

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

    
174
    def clean_recaptcha_challenge_field(self):
175
        if 'recaptcha_response_field' in self.cleaned_data:
176
            self.validate_captcha()
177
        return self.cleaned_data['recaptcha_challenge_field']
178

    
179
    def validate_captcha(self):
180
        rcf = self.cleaned_data['recaptcha_challenge_field']
181
        rrf = self.cleaned_data['recaptcha_response_field']
182
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
183
        if not check.is_valid:
184
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
185

    
186
    def post_store_user(self, user, request):
187
        """
188
        Interface method for descendant backends to be able to do stuff within
189
        the transaction enabled by store_user.
190
        """
191
        user.add_auth_provider('local', auth_backend='astakos')
192
        user.set_password(self.cleaned_data['password1'])
193

    
194
    def save(self, commit=True):
195
        """
196
        Saves the email, first_name and last_name properties, after the normal
197
        save behavior is complete.
198
        """
199
        user = super(LocalUserCreationForm, self).save(commit=False)
200
        user.renew_token()
201
        if commit:
202
            user.save()
203
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
204
        return user
205

    
206

    
207
class InvitedLocalUserCreationForm(LocalUserCreationForm):
208
    """
209
    Extends the LocalUserCreationForm: email is readonly.
210
    """
211
    class Meta:
212
        model = AstakosUser
213
        fields = ("email", "first_name", "last_name", "has_signed_terms")
214

    
215
    def __init__(self, *args, **kwargs):
216
        """
217
        Changes the order of fields, and removes the username field.
218
        """
219
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
220

    
221
        #set readonly form fields
222
        ro = ('email', 'username',)
223
        for f in ro:
224
            self.fields[f].widget.attrs['readonly'] = True
225

    
226
    def save(self, commit=True):
227
        user = super(InvitedLocalUserCreationForm, self).save(commit=False)
228
        user.set_invitations_level()
229
        user.email_verified = True
230
        if commit:
231
            user.save()
232
        return user
233

    
234

    
235
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
236
    id = forms.CharField(
237
        widget=forms.HiddenInput(),
238
        label='',
239
        required=False
240
    )
241
    third_party_identifier = forms.CharField(
242
        widget=forms.HiddenInput(),
243
        label=''
244
    )
245

    
246
    class Meta:
247
        model = AstakosUser
248
        fields = ['id', 'email', 'third_party_identifier',
249
                  'first_name', 'last_name', 'has_signed_terms']
250

    
251
    def __init__(self, *args, **kwargs):
252
        """
253
        Changes the order of fields, and removes the username field.
254
        """
255
        self.request = kwargs.get('request', None)
256
        if self.request:
257
            kwargs.pop('request')
258

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

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

    
269
    def clean_email(self):
270
        email = self.cleaned_data['email']
271
        if not email:
272
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
273
        if reserved_verified_email(email):
274
            provider = auth_providers.get_provider(self.request.REQUEST.get('provider', 'local'))
275
            extra_message = _(astakos_messages.EXISTING_EMAIL_THIRD_PARTY_NOTIFICATION) % \
276
                    (provider.get_title_display, reverse('edit_profile'))
277

    
278
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED) + ' ' + \
279
                                        extra_message)
280
        return email
281

    
282
    def clean_has_signed_terms(self):
283
        has_signed_terms = self.cleaned_data['has_signed_terms']
284
        if not has_signed_terms:
285
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
286
        return has_signed_terms
287

    
288
    def post_store_user(self, user, request):
289
        pending = PendingThirdPartyUser.objects.get(
290
                                token=request.POST.get('third_party_token'),
291
                                third_party_identifier= \
292
                            self.cleaned_data.get('third_party_identifier'))
293
        return user.add_pending_auth_provider(pending)
294

    
295
    def save(self, commit=True):
296
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
297
        user.set_unusable_password()
298
        user.renew_token()
299
        if commit:
300
            user.save()
301
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
302
        return user
303

    
304

    
305
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
306
    """
307
    Extends the ThirdPartyUserCreationForm: email is readonly.
308
    """
309
    def __init__(self, *args, **kwargs):
310
        """
311
        Changes the order of fields, and removes the username field.
312
        """
313
        super(
314
            InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
315

    
316
        #set readonly form fields
317
        ro = ('email',)
318
        for f in ro:
319
            self.fields[f].widget.attrs['readonly'] = True
320

    
321
    def save(self, commit=True):
322
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
323
        user.set_invitation_level()
324
        user.email_verified = True
325
        if commit:
326
            user.save()
327
        return user
328

    
329

    
330
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
331
    additional_email = forms.CharField(
332
        widget=forms.HiddenInput(), label='', required=False)
333

    
334
    def __init__(self, *args, **kwargs):
335
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
336
        # copy email value to additional_mail in case user will change it
337
        name = 'email'
338
        field = self.fields[name]
339
        self.initial['additional_email'] = self.initial.get(name, field.initial)
340
        self.initial['email'] = None
341

    
342

    
343
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
344
                                        InvitedThirdPartyUserCreationForm):
345
    pass
346

    
347

    
348
class LoginForm(AuthenticationForm):
349
    username = forms.EmailField(label=_("Email"))
350
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
351
    recaptcha_response_field = forms.CharField(
352
        widget=RecaptchaWidget, label='')
353

    
354
    def __init__(self, *args, **kwargs):
355
        was_limited = kwargs.get('was_limited', False)
356
        request = kwargs.get('request', None)
357
        if request:
358
            self.ip = request.META.get('REMOTE_ADDR',
359
                                       request.META.get('HTTP_X_REAL_IP', None))
360

    
361
        t = ('request', 'was_limited')
362
        for elem in t:
363
            if elem in kwargs.keys():
364
                kwargs.pop(elem)
365
        super(LoginForm, self).__init__(*args, **kwargs)
366

    
367
        self.fields.keyOrder = ['username', 'password']
368
        if was_limited and RECAPTCHA_ENABLED:
369
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
370
                                         'recaptcha_response_field', ])
371

    
372
    def clean_username(self):
373
        return self.cleaned_data['username'].lower()
374

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

    
380
    def clean_recaptcha_challenge_field(self):
381
        if 'recaptcha_response_field' in self.cleaned_data:
382
            self.validate_captcha()
383
        return self.cleaned_data['recaptcha_challenge_field']
384

    
385
    def validate_captcha(self):
386
        rcf = self.cleaned_data['recaptcha_challenge_field']
387
        rrf = self.cleaned_data['recaptcha_response_field']
388
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
389
        if not check.is_valid:
390
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
391

    
392
    def clean(self):
393
        """
394
        Override default behavior in order to check user's activation later
395
        """
396
        username = self.cleaned_data.get('username')
397

    
398
        if username:
399
            try:
400
                user = AstakosUser.objects.get_by_identifier(username)
401
                if not user.has_auth_provider('local'):
402
                    provider = auth_providers.get_provider('local')
403
                    raise forms.ValidationError(
404
                        _(provider.get_message('NOT_ACTIVE_FOR_USER')))
405
            except AstakosUser.DoesNotExist:
406
                pass
407

    
408
        try:
409
            super(LoginForm, self).clean()
410
        except forms.ValidationError, e:
411
            if self.user_cache is None:
412
                raise
413
            if not self.user_cache.is_active:
414
                raise forms.ValidationError(self.user_cache.get_inactive_message())
415
            if self.request:
416
                if not self.request.session.test_cookie_worked():
417
                    raise
418
        return self.cleaned_data
419

    
420

    
421
class ProfileForm(forms.ModelForm):
422
    """
423
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
424
    Most of the fields are readonly since the user is not allowed to change
425
    them.
426

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

    
432
    class Meta:
433
        model = AstakosUser
434
        fields = ('email', 'first_name', 'last_name', 'auth_token',
435
                  'auth_token_expires')
436

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

    
446
    def save(self, commit=True):
447
        user = super(ProfileForm, self).save(commit=False)
448
        user.is_verified = True
449
        if self.cleaned_data.get('renew'):
450
            user.renew_token(
451
                flush_sessions=True,
452
                current_key=self.session_key
453
            )
454
        if commit:
455
            user.save()
456
        return user
457

    
458

    
459
class FeedbackForm(forms.Form):
460
    """
461
    Form for writing feedback.
462
    """
463
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
464
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
465
                                    required=False)
466

    
467

    
468
class SendInvitationForm(forms.Form):
469
    """
470
    Form for sending an invitations
471
    """
472

    
473
    email = forms.EmailField(required=True, label='Email address')
474
    first_name = forms.EmailField(label='First name')
475
    last_name = forms.EmailField(label='Last name')
476

    
477

    
478
class ExtendedPasswordResetForm(PasswordResetForm):
479
    """
480
    Extends PasswordResetForm by overriding
481

482
    save method: to pass a custom from_email in send_mail.
483
    clean_email: to handle local auth provider checks
484
    """
485
    def clean_email(self):
486
        email = super(ExtendedPasswordResetForm, self).clean_email()
487
        try:
488
            user = AstakosUser.objects.get_by_identifier(email)
489

    
490
            if not user.is_active:
491
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
492

    
493
            if not user.has_usable_password():
494
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
495

    
496
            if not user.can_change_password():
497
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
498
        except AstakosUser.DoesNotExist, e:
499
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
500
        return email
501

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

    
524

    
525
class EmailChangeForm(forms.ModelForm):
526

    
527
    class Meta:
528
        model = EmailChange
529
        fields = ('new_email_address',)
530

    
531
    def clean_new_email_address(self):
532
        addr = self.cleaned_data['new_email_address']
533
        if reserved_verified_email(addr):
534
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
535
        return addr
536

    
537
    def save(self, email_template_name, request, commit=True):
538
        ec = super(EmailChangeForm, self).save(commit=False)
539
        ec.user = request.user
540
        activation_key = hashlib.sha1(
541
            str(random()) + smart_str(ec.new_email_address))
542
        ec.activation_key = activation_key.hexdigest()
543
        if commit:
544
            ec.save()
545
        send_change_email(ec, request, email_template_name=email_template_name)
546

    
547

    
548
class SignApprovalTermsForm(forms.ModelForm):
549

    
550
    class Meta:
551
        model = AstakosUser
552
        fields = ("has_signed_terms",)
553

    
554
    def __init__(self, *args, **kwargs):
555
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
556

    
557
    def clean_has_signed_terms(self):
558
        has_signed_terms = self.cleaned_data['has_signed_terms']
559
        if not has_signed_terms:
560
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
561
        return has_signed_terms
562

    
563

    
564
class InvitationForm(forms.ModelForm):
565

    
566
    username = forms.EmailField(label=_("Email"))
567

    
568
    def __init__(self, *args, **kwargs):
569
        super(InvitationForm, self).__init__(*args, **kwargs)
570

    
571
    class Meta:
572
        model = Invitation
573
        fields = ('username', 'realname')
574

    
575
    def clean_username(self):
576
        username = self.cleaned_data['username']
577
        try:
578
            Invitation.objects.get(username=username)
579
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
580
        except Invitation.DoesNotExist:
581
            pass
582
        return username
583

    
584

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

    
595
    def __init__(self, user, *args, **kwargs):
596
        self.session_key = kwargs.pop('session_key', None)
597
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
598

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

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

    
621
    def __init__(self, user, *args, **kwargs):
622
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
623

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

    
634
        except BaseException, e:
635
            logger.exception(e)
636
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
637

    
638

    
639

    
640

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

    
656

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

    
666
app_desc_label       =  _("Description")
667
app_desc_help        =  _("""
668
        Please provide a short but descriptive abstract of your Project,
669
        so that anyone searching can quickly understand
670
        what this Project is about.""")
671

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

    
680
app_start_date_label =  _("Start date")
681
app_start_date_help  =  _("""
682
        Provide a date when your need your project to be created,
683
        and members to be able to join and get resources.
684
        This date is only a hint to help prioritize reviews.""")
685

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

    
694
join_policy_label    =  _("Joining policy")
695
leave_policy_label   =  _("Leaving policy")
696

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

    
706
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
707
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
708

    
709
class ProjectApplicationForm(forms.ModelForm):
710

    
711
    name = forms.CharField(
712
        label     = app_name_label,
713
        help_text = app_name_help,
714
        widget    = app_name_widget,
715
        validators = [app_name_validator])
716

    
717
    homepage = forms.URLField(
718
        label     = app_home_label,
719
        help_text = app_home_help,
720
        widget    = app_home_widget,
721
        required  = False)
722

    
723
    description = forms.CharField(
724
        label     = app_desc_label,
725
        help_text = app_desc_help,
726
        widget    = forms.Textarea,
727
        required  = False)
728

    
729
    comments = forms.CharField(
730
        label     = app_comment_label,
731
        help_text = app_comment_help,
732
        widget    = forms.Textarea,
733
        required  = False)
734

    
735
    start_date = forms.DateTimeField(
736
        label     = app_start_date_label,
737
        help_text = app_start_date_help,
738
        required  = False)
739

    
740
    end_date = forms.DateTimeField(
741
        label     = app_end_date_label,
742
        help_text = app_end_date_help)
743

    
744
    member_join_policy  = forms.TypedChoiceField(
745
        label     = join_policy_label,
746
        initial   = 2,
747
        coerce    = int,
748
        choices   = join_policies)
749

    
750
    member_leave_policy = forms.TypedChoiceField(
751
        label     = leave_policy_label,
752
        coerce    = int,
753
        choices   = leave_policies)
754

    
755
    limit_on_members_number = forms.IntegerField(
756
        label     = max_members_label,
757
        help_text = max_members_help,
758
        required  = False)
759

    
760
    class Meta:
761
        model = ProjectApplication
762
        fields = ( 'name', 'homepage', 'description',
763
                    'start_date', 'end_date', 'comments',
764
                    'member_join_policy', 'member_leave_policy',
765
                    'limit_on_members_number')
766

    
767
    def __init__(self, *args, **kwargs):
768
        instance = kwargs.get('instance')
769
        self.precursor_application = instance
770
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
771
        # in case of new application remove closed join policy
772
        if not instance:
773
            policies = PROJECT_MEMBER_JOIN_POLICIES.copy()
774
            policies.pop('3')
775
            self.fields['member_join_policy'].choices = policies.iteritems()
776

    
777
    def clean_start_date(self):
778
        start_date = self.cleaned_data.get('start_date')
779
        if not self.precursor_application:
780
            if start_date and (start_date - today).days < 0:
781
                raise forms.ValidationError(
782
                _(astakos_messages.INVALID_PROJECT_START_DATE))
783
        return start_date
784

    
785
    def clean_end_date(self):
786
        start_date = self.cleaned_data.get('start_date')
787
        end_date = self.cleaned_data.get('end_date')
788
        if end_date and (end_date - today).days < 0:
789
            raise forms.ValidationError(
790
                _(astakos_messages.INVALID_PROJECT_END_DATE))
791
        if start_date and (end_date - start_date).days <= 0:
792
            raise forms.ValidationError(
793
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
794
        return end_date
795

    
796
    def clean(self):
797
        userid = self.data.get('user', None)
798
        self.user = None
799
        if userid:
800
            try:
801
                self.user = AstakosUser.objects.get(id=userid)
802
            except AstakosUser.DoesNotExist:
803
                pass
804
        if not self.user:
805
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
806
        super(ProjectApplicationForm, self).clean()
807
        return self.cleaned_data
808

    
809
    @property
810
    def resource_policies(self):
811
        policies = []
812
        append = policies.append
813
        for name, value in self.data.iteritems():
814
            if not value:
815
                continue
816
            uplimit = value
817
            if name.endswith('_uplimit'):
818
                subs = name.split('_uplimit')
819
                prefix, suffix = subs
820
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
821
                resource = Resource.objects.get(service__name=s, name=r)
822

    
823
                # keep only resource limits for selected resource groups
824
                if self.data.get(
825
                    'is_selected_%s' % resource.group, "0"
826
                 ) == "1":
827
                    d = model_to_dict(resource)
828
                    if uplimit:
829
                        d.update(dict(service=s, resource=r, uplimit=uplimit))
830
                    else:
831
                        d.update(dict(service=s, resource=r, uplimit=None))
832
                    append(d)
833

    
834
        return policies
835

    
836
    def save(self, commit=True):
837
        data = dict(self.cleaned_data)
838
        data['precursor_application'] = self.instance.id
839
        data['owner'] = self.user
840
        data['resource_policies'] = self.resource_policies
841
        submit_application(data, request_user=self.user)
842

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

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

    
871
    def __init__(self, *args, **kwargs):
872
        application_id = kwargs.pop('application_id', None)
873
        if application_id:
874
            self.project = Project.objects.get(application__id=application_id)
875
        self.request_user = kwargs.pop('request_user', None)
876
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
877

    
878
    def clean(self):
879
        try:
880
            do_accept_membership_checks(self.project, self.request_user)
881
        except PermissionDenied, e:
882
            raise forms.ValidationError(e)
883

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

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

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

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