Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 8172d87d

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',
246
                  'first_name', 'last_name', 'has_signed_terms']
247

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

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

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

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

    
275
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED) + ' ' + \
276
                                        extra_message)
277
        return email
278

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

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

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

    
301

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

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

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

    
326

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

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

    
339

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

    
344

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

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

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

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

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

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

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

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

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

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

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

    
417

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

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

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

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

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

    
455

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

    
464

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

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

    
474

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

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

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

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

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

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

    
521

    
522
class EmailChangeForm(forms.ModelForm):
523

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

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

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

    
544

    
545
class SignApprovalTermsForm(forms.ModelForm):
546

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

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

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

    
560

    
561
class InvitationForm(forms.ModelForm):
562

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

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

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

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

    
581

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

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

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

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

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

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

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

    
635

    
636

    
637

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

    
653

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

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

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

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

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

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

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

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

    
706
class ProjectApplicationForm(forms.ModelForm):
707

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
835
        return policies
836

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

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

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

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

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

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

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

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

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