Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 564a2292

History | View | Annotate | Download (33.6 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
class StoreUserMixin(object):
90

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

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

    
106

    
107
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
108
    """
109
    Extends the built in UserCreationForm in several ways:
110

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

    
120
    class Meta:
121
        model = AstakosUser
122
        fields = ("email", "first_name", "last_name",
123
                  "has_signed_terms", "has_signed_terms")
124

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

    
134
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
135
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
136
                                'password1', 'password2']
137

    
138
        if RECAPTCHA_ENABLED:
139
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
140
                                         'recaptcha_response_field', ])
141
        if get_latest_terms():
142
            self.fields.keyOrder.append('has_signed_terms')
143

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

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

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

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

    
171
    def clean_recaptcha_challenge_field(self):
172
        if 'recaptcha_response_field' in self.cleaned_data:
173
            self.validate_captcha()
174
        return self.cleaned_data['recaptcha_challenge_field']
175

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

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

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

    
203

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

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

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

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

    
231

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

    
243
    class Meta:
244
        model = AstakosUser
245
        fields = ['id', 'email', 'third_party_identifier', 'first_name', 'last_name']
246

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

    
255
        latest_terms = get_latest_terms()
256
        if latest_terms:
257
            self._meta.fields.append('has_signed_terms')
258

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

    
261
        if latest_terms:
262
            self.fields.keyOrder.append('has_signed_terms')
263

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

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

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

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

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

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

    
307

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

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

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

    
332

    
333
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
334
    additional_email = forms.CharField(
335
        widget=forms.HiddenInput(), label='', required=False)
336

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

    
345

    
346
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
347
                                        InvitedThirdPartyUserCreationForm):
348
    pass
349

    
350

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

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

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

    
370
        self.fields.keyOrder = ['username', 'password']
371
        if was_limited and RECAPTCHA_ENABLED:
372
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
373
                                         'recaptcha_response_field', ])
374

    
375
    def clean_username(self):
376
        return self.cleaned_data['username'].lower()
377

    
378
    def clean_recaptcha_response_field(self):
379
        if 'recaptcha_challenge_field' in self.cleaned_data:
380
            self.validate_captcha()
381
        return self.cleaned_data['recaptcha_response_field']
382

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

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

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

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

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

    
423

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

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

    
435
    class Meta:
436
        model = AstakosUser
437
        fields = ('email', 'first_name', 'last_name', 'auth_token',
438
                  'auth_token_expires')
439

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

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

    
461

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

    
470

    
471
class SendInvitationForm(forms.Form):
472
    """
473
    Form for sending an invitations
474
    """
475

    
476
    email = forms.EmailField(required=True, label='Email address')
477
    first_name = forms.EmailField(label='First name')
478
    last_name = forms.EmailField(label='Last name')
479

    
480

    
481
class ExtendedPasswordResetForm(PasswordResetForm):
482
    """
483
    Extends PasswordResetForm by overriding
484

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

    
493
            if not user.is_active:
494
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
495

    
496
            if not user.has_usable_password():
497
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
498

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

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

    
527

    
528
class EmailChangeForm(forms.ModelForm):
529

    
530
    class Meta:
531
        model = EmailChange
532
        fields = ('new_email_address',)
533

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

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

    
550

    
551
class SignApprovalTermsForm(forms.ModelForm):
552

    
553
    class Meta:
554
        model = AstakosUser
555
        fields = ("has_signed_terms",)
556

    
557
    def __init__(self, *args, **kwargs):
558
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
559

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

    
566

    
567
class InvitationForm(forms.ModelForm):
568

    
569
    username = forms.EmailField(label=_("Email"))
570

    
571
    def __init__(self, *args, **kwargs):
572
        super(InvitationForm, self).__init__(*args, **kwargs)
573

    
574
    class Meta:
575
        model = Invitation
576
        fields = ('username', 'realname')
577

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

    
587

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

    
598
    def __init__(self, user, *args, **kwargs):
599
        self.session_key = kwargs.pop('session_key', None)
600
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
601

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

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

    
624
    def __init__(self, user, *args, **kwargs):
625
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
626

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

    
637
        except BaseException, e:
638
            logger.exception(e)
639
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
640

    
641

    
642

    
643

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

    
659

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

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

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

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

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

    
697
join_policy_label    =  _("Joining policy")
698
leave_policy_label   =  _("Leaving policy")
699

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

    
709
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
710
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
711

    
712
class ProjectApplicationForm(forms.ModelForm):
713

    
714
    name = forms.CharField(
715
        label     = app_name_label,
716
        help_text = app_name_help,
717
        widget    = app_name_widget,
718
        validators = [app_name_validator])
719

    
720
    homepage = forms.URLField(
721
        label     = app_home_label,
722
        help_text = app_home_help,
723
        widget    = app_home_widget,
724
        required  = False)
725

    
726
    description = forms.CharField(
727
        label     = app_desc_label,
728
        help_text = app_desc_help,
729
        widget    = forms.Textarea,
730
        required  = False)
731

    
732
    comments = forms.CharField(
733
        label     = app_comment_label,
734
        help_text = app_comment_help,
735
        widget    = forms.Textarea,
736
        required  = False)
737

    
738
    start_date = forms.DateTimeField(
739
        label     = app_start_date_label,
740
        help_text = app_start_date_help,
741
        required  = False)
742

    
743
    end_date = forms.DateTimeField(
744
        label     = app_end_date_label,
745
        help_text = app_end_date_help)
746

    
747
    member_join_policy  = forms.TypedChoiceField(
748
        label     = join_policy_label,
749
        initial   = 2,
750
        coerce    = int,
751
        choices   = join_policies)
752

    
753
    member_leave_policy = forms.TypedChoiceField(
754
        label     = leave_policy_label,
755
        coerce    = int,
756
        choices   = leave_policies)
757

    
758
    limit_on_members_number = forms.IntegerField(
759
        label     = max_members_label,
760
        help_text = max_members_help,
761
        required  = False)
762

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

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

    
780
    def clean_start_date(self):
781
        start_date = self.cleaned_data.get('start_date')
782
        if not self.precursor_application:
783
            if start_date and (start_date - datetime.now()).days < 0:
784
                raise forms.ValidationError(
785
                _(astakos_messages.INVALID_PROJECT_START_DATE))
786
        return start_date
787

    
788
    def clean_end_date(self):
789
        start_date = self.cleaned_data.get('start_date')
790
        end_date = self.cleaned_data.get('end_date')
791
        now = datetime.now()
792
        if end_date and (end_date - now).days < 0:
793
            raise forms.ValidationError(
794
                _(astakos_messages.INVALID_PROJECT_END_DATE))
795
        if start_date and (end_date - start_date).days <= 0:
796
            raise forms.ValidationError(
797
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
798
        return end_date
799

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

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

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

    
838
        return policies
839

    
840
    def save(self, commit=True):
841
        data = dict(self.cleaned_data)
842
        data['precursor_application'] = self.instance.id
843
        data['owner'] = self.user
844
        data['resource_policies'] = self.resource_policies
845
        submit_application(data, request_user=self.user)
846

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

    
870
class AddProjectMembersForm(forms.Form):
871
    q = forms.CharField(
872
        max_length=800, widget=forms.Textarea, label=_('Add members'),
873
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
874

    
875
    def __init__(self, *args, **kwargs):
876
        application_id = kwargs.pop('application_id', None)
877
        if application_id:
878
            self.project = Project.objects.get(application__id=application_id)
879
        self.request_user = kwargs.pop('request_user', None)
880
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
881

    
882
    def clean(self):
883
        try:
884
            do_accept_membership_checks(self.project, self.request_user)
885
        except PermissionDenied, e:
886
            raise forms.ValidationError(e)
887

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

    
899
    def get_valid_users(self):
900
        """Should be called after form cleaning"""
901
        try:
902
            return self.valid_users
903
        except:
904
            return ()
905

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

    
916
class ProjectSearchForm(forms.Form):
917
    q = forms.CharField(max_length=200, label='Search project', required=False)
918