Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (38 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, get_connection
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, EMAILCHANGE_ENABLED)
68
from astakos.im.widgets import DummyWidget, RecaptchaWidget
69
from astakos.im.functions import (
70
    send_change_email, submit_application, 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

    
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)),
521
                      from_email,
522
                      [user.email],
523
                      connection=get_connection)
524

    
525

    
526
class EmailChangeForm(forms.ModelForm):
527

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

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

    
538
    def save(self, request, email_template_name='registration/email_change_email.txt', commit=True):
539
        ec = super(EmailChangeForm, self).save(commit=False)
540
        ec.user = request.user
541
        # delete pending email changes
542
        request.user.emailchanges.all().delete()
543

    
544
        activation_key = hashlib.sha1(
545
            str(random()) + smart_str(ec.new_email_address))
546
        ec.activation_key = activation_key.hexdigest()
547
        if commit:
548
            ec.save()
549
        send_change_email(ec, request, email_template_name=email_template_name)
550

    
551

    
552
class SignApprovalTermsForm(forms.ModelForm):
553

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

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

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

    
567

    
568
class InvitationForm(forms.ModelForm):
569

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

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

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

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

    
588

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

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

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

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

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

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

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

    
642

    
643

    
644

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

    
660

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

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

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

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

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

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

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

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

    
713
class ProjectApplicationForm(forms.ModelForm):
714

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

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

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

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

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

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

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

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

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

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

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

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

    
791
    def clean_end_date(self):
792
        start_date = self.cleaned_data.get('start_date')
793
        end_date = self.cleaned_data.get('end_date')
794
        today = datetime.now()
795
        today = datetime(today.year, today.month, today.day)
796
        if end_date and (end_date - today).days < 0:
797
            raise forms.ValidationError(
798
                _(astakos_messages.INVALID_PROJECT_END_DATE))
799
        if start_date and (end_date - start_date).days <= 0:
800
            raise forms.ValidationError(
801
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
802
        return end_date
803

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

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

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

    
842
        return policies
843

    
844
    def save(self, commit=True):
845
        data = dict(self.cleaned_data)
846
        data['precursor_application'] = self.instance.id
847
        data['owner'] = self.user
848
        data['resource_policies'] = self.resource_policies
849
        submit_application(data, request_user=self.user)
850

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

    
874
class AddProjectMembersForm(forms.Form):
875
    q = forms.CharField(
876
        max_length=800, widget=forms.Textarea, label=_('Add members'),
877
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
878

    
879
    def __init__(self, *args, **kwargs):
880
        chain_id = kwargs.pop('chain_id', None)
881
        if chain_id:
882
            self.project = Project.objects.get(id=chain_id)
883
        self.request_user = kwargs.pop('request_user', None)
884
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
885

    
886
    def clean(self):
887
        try:
888
            accept_membership_checks(self.project, self.request_user)
889
        except PermissionDenied, e:
890
            raise forms.ValidationError(e)
891

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

    
903
    def get_valid_users(self):
904
        """Should be called after form cleaning"""
905
        try:
906
            return self.valid_users
907
        except:
908
            return ()
909

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

    
920

    
921
class ProjectSearchForm(forms.Form):
922
    q = forms.CharField(max_length=200, label='Search project', required=False)
923

    
924

    
925
class ExtendedProfileForm(ProfileForm):
926
    """
927
    Profile form that combines `email change` and `password change` user
928
    actions by propagating submited data to internal EmailChangeForm
929
    and ExtendedPasswordChangeForm objects.
930
    """
931

    
932
    password_change_form = None
933
    email_change_form = None
934

    
935
    password_change = False
936
    email_change = False
937

    
938
    extra_forms_fields = {
939
        'email': ['new_email_address'],
940
        'password': ['old_password', 'new_password1', 'new_password2']
941
    }
942

    
943
    fields = ('email')
944
    change_password = forms.BooleanField(initial=False, required=False)
945
    change_email = forms.BooleanField(initial=False, required=False)
946

    
947
    email_changed = False
948
    password_changed = False
949

    
950
    def __init__(self, *args, **kwargs):
951
        session_key = kwargs.get('session_key', None)
952
        self.fields_list = [
953
                'email',
954
                'new_email_address',
955
                'first_name',
956
                'last_name',
957
                'auth_token',
958
                'auth_token_expires',
959
                'old_password',
960
                'new_password1',
961
                'new_password2',
962
                'change_email',
963
                'change_password',
964
        ]
965

    
966
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
967
        self.session_key = session_key
968
        if self.instance.can_change_password():
969
            self.password_change = True
970
        else:
971
            self.fields_list.remove('old_password')
972
            self.fields_list.remove('new_password1')
973
            self.fields_list.remove('new_password2')
974
            self.fields_list.remove('change_password')
975
            del self.fields['change_password']
976

    
977

    
978
        if EMAILCHANGE_ENABLED and self.instance.can_change_email():
979
            self.email_change = True
980
        else:
981
            self.fields_list.remove('new_email_address')
982
            self.fields_list.remove('change_email')
983
            del self.fields['change_email']
984

    
985
        self._init_extra_forms()
986
        self.save_extra_forms = []
987
        self.success_messages = []
988
        self.fields.keyOrder = self.fields_list
989

    
990

    
991
    def _init_extra_form_fields(self):
992
        
993

    
994
        if self.email_change:
995
            self.fields.update(self.email_change_form.fields)
996
            self.fields['new_email_address'].required = False
997

    
998
        if self.password_change:
999
            self.fields.update(self.password_change_form.fields)
1000
            self.fields['old_password'].required = False
1001
            self.fields['old_password'].label = _('Password')
1002
            self.fields['old_password'].initial = 'skata'
1003
            self.fields['new_password1'].required = False
1004
            self.fields['new_password2'].required = False
1005

    
1006
    def _update_extra_form_errors(self):
1007
        if self.cleaned_data.get('change_password'):
1008
            self.errors.update(self.password_change_form.errors)
1009
        if self.cleaned_data.get('change_email'):
1010
            self.errors.update(self.email_change_form.errors)
1011

    
1012
    def _init_extra_forms(self):
1013
        self.email_change_form = EmailChangeForm(self.data)
1014
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1015
                                   data=self.data, session_key=self.session_key)
1016
        self._init_extra_form_fields()
1017

    
1018
    def is_valid(self):
1019
        password, email = True, True
1020
        profile = super(ExtendedProfileForm, self).is_valid()
1021
        if profile and self.cleaned_data.get('change_password', None):
1022
            password = self.password_change_form.is_valid()
1023
            self.save_extra_forms.append('password')
1024
        if profile and self.cleaned_data.get('change_email'):
1025
            email = self.email_change_form.is_valid()
1026
            self.save_extra_forms.append('email')
1027

    
1028
        if not password or not email:
1029
            self._update_extra_form_errors()
1030

    
1031
        return all([profile, password, email])
1032

    
1033
    def save(self, request, *args, **kwargs):
1034
        if 'email' in self.save_extra_forms:
1035
            self.email_change_form.save(request, *args, **kwargs)
1036
            self.email_changed = True
1037
        if 'password' in self.save_extra_forms:
1038
            self.password_change_form.save(*args, **kwargs)
1039
            self.password_changed = True
1040
        return super(ExtendedProfileForm, self).save(*args, **kwargs)
1041