Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33.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
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
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
278
        return email
279

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

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

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

    
302

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

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

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

    
327

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

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

    
340

    
341
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
342
                                        InvitedThirdPartyUserCreationForm):
343
    pass
344

    
345

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

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

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

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

    
370
    def clean_username(self):
371
        return self.cleaned_data['username'].lower()
372

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

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

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

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

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

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

    
418

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

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

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

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

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

    
456

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

    
465

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

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

    
475

    
476
class ExtendedPasswordResetForm(PasswordResetForm):
477
    """
478
    Extends PasswordResetForm by overriding
479

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

    
488
            if not user.is_active:
489
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
490

    
491
            if not user.has_usable_password():
492
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
493

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

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

    
522

    
523
class EmailChangeForm(forms.ModelForm):
524

    
525
    class Meta:
526
        model = EmailChange
527
        fields = ('new_email_address',)
528

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

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

    
545

    
546
class SignApprovalTermsForm(forms.ModelForm):
547

    
548
    class Meta:
549
        model = AstakosUser
550
        fields = ("has_signed_terms",)
551

    
552
    def __init__(self, *args, **kwargs):
553
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
554

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

    
561

    
562
class InvitationForm(forms.ModelForm):
563

    
564
    username = forms.EmailField(label=_("Email"))
565

    
566
    def __init__(self, *args, **kwargs):
567
        super(InvitationForm, self).__init__(*args, **kwargs)
568

    
569
    class Meta:
570
        model = Invitation
571
        fields = ('username', 'realname')
572

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

    
582

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

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

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

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

    
619
    def __init__(self, user, *args, **kwargs):
620
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
621

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

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

    
636

    
637

    
638

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

    
654

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

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

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

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

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

    
692
join_policy_label    =  _("Joining policy")
693
leave_policy_label   =  _("Leaving policy")
694

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

    
704
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
705
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
706

    
707
class ProjectApplicationForm(forms.ModelForm):
708

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

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

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

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

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

    
738
    end_date = forms.DateTimeField(
739
        label     = app_end_date_label,
740
        help_text = app_end_date_help)
741

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

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

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

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

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

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

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

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

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

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

    
833
        return policies
834

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

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

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

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

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

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

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

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

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