Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 28f4439f

History | View | Annotate | Download (39 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
    email = forms.EmailField(
243
        label='Contact email',
244
        help_text = 'This is needed for contact purposes. It doesn&#39;t need to be the same with the one you provided to login previously. '
245
    )
246

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

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

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

    
262
        if not get_latest_terms():
263
            del self.fields['has_signed_terms']
264

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

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

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

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

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

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

    
308

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

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

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

    
333

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

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

    
346

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

    
351

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

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

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

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

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

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

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

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

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

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

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

    
424

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

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

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

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

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

    
462

    
463

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

    
472

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

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

    
482

    
483
class ExtendedPasswordResetForm(PasswordResetForm):
484
    """
485
    Extends PasswordResetForm by overriding
486

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

    
495
            if not user.is_active:
496
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
497

    
498
            if not user.has_usable_password():
499
                provider = auth_providers.get_provider('local')
500
                available_providers = user.auth_providers.all()
501
                available_providers = ",".join(p.settings.get_title_display for p in \
502
                                                   available_providers)
503
                message = astakos_messages.UNUSABLE_PASSWORD % \
504
                    (provider.get_method_prompt_display, available_providers)
505
                raise forms.ValidationError(message)
506

    
507
            if not user.can_change_password():
508
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
509
        except AstakosUser.DoesNotExist, e:
510
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
511
        return email
512

    
513
    def save(
514
        self, domain_override=None, email_template_name='registration/password_reset_email.html',
515
            use_https=False, token_generator=default_token_generator, request=None):
516
        """
517
        Generates a one-use only link for resetting password and sends to the user.
518
        """
519
        for user in self.users_cache:
520
            url = user.astakosuser.get_password_reset_url(token_generator)
521
            url = urljoin(BASEURL, url)
522
            t = loader.get_template(email_template_name)
523
            c = {
524
                'email': user.email,
525
                'url': url,
526
                'site_name': SITENAME,
527
                'user': user,
528
                'baseurl': BASEURL,
529
                'support': DEFAULT_CONTACT_EMAIL
530
            }
531
            from_email = settings.SERVER_EMAIL
532
            send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
533
                      t.render(Context(c)),
534
                      from_email,
535
                      [user.email],
536
                      connection=get_connection())
537

    
538

    
539
class EmailChangeForm(forms.ModelForm):
540

    
541
    class Meta:
542
        model = EmailChange
543
        fields = ('new_email_address',)
544

    
545
    def clean_new_email_address(self):
546
        addr = self.cleaned_data['new_email_address']
547
        if reserved_verified_email(addr):
548
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
549
        return addr
550

    
551
    def save(self, request, email_template_name='registration/email_change_email.txt', commit=True):
552
        ec = super(EmailChangeForm, self).save(commit=False)
553
        ec.user = request.user
554
        # delete pending email changes
555
        request.user.emailchanges.all().delete()
556

    
557
        activation_key = hashlib.sha1(
558
            str(random()) + smart_str(ec.new_email_address))
559
        ec.activation_key = activation_key.hexdigest()
560
        if commit:
561
            ec.save()
562
        send_change_email(ec, request, email_template_name=email_template_name)
563

    
564

    
565
class SignApprovalTermsForm(forms.ModelForm):
566

    
567
    class Meta:
568
        model = AstakosUser
569
        fields = ("has_signed_terms",)
570

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

    
574
    def clean_has_signed_terms(self):
575
        has_signed_terms = self.cleaned_data['has_signed_terms']
576
        if not has_signed_terms:
577
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
578
        return has_signed_terms
579

    
580

    
581
class InvitationForm(forms.ModelForm):
582

    
583
    username = forms.EmailField(label=_("Email"))
584

    
585
    def __init__(self, *args, **kwargs):
586
        super(InvitationForm, self).__init__(*args, **kwargs)
587

    
588
    class Meta:
589
        model = Invitation
590
        fields = ('username', 'realname')
591

    
592
    def clean_username(self):
593
        username = self.cleaned_data['username']
594
        try:
595
            Invitation.objects.get(username=username)
596
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
597
        except Invitation.DoesNotExist:
598
            pass
599
        return username
600

    
601

    
602
class ExtendedPasswordChangeForm(PasswordChangeForm):
603
    """
604
    Extends PasswordChangeForm by enabling user
605
    to optionally renew also the token.
606
    """
607
    if not NEWPASSWD_INVALIDATE_TOKEN:
608
        renew = forms.BooleanField(label='Renew token', required=False,
609
                                   initial=True,
610
                                   help_text='Unsetting this may result in security risk.')
611

    
612
    def __init__(self, user, *args, **kwargs):
613
        self.session_key = kwargs.pop('session_key', None)
614
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
615

    
616
    def save(self, commit=True):
617
        try:
618
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
619
                self.user.renew_token()
620
            self.user.flush_sessions(current_key=self.session_key)
621
        except AttributeError:
622
            # if user model does has not such methods
623
            pass
624
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
625

    
626
class ExtendedSetPasswordForm(SetPasswordForm):
627
    """
628
    Extends SetPasswordForm by enabling user
629
    to optionally renew also the token.
630
    """
631
    if not NEWPASSWD_INVALIDATE_TOKEN:
632
        renew = forms.BooleanField(
633
            label='Renew token',
634
            required=False,
635
            initial=True,
636
            help_text='Unsetting this may result in security risk.')
637

    
638
    def __init__(self, user, *args, **kwargs):
639
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
640

    
641
    @transaction.commit_on_success()
642
    def save(self, commit=True):
643
        try:
644
            self.user = AstakosUser.objects.get(id=self.user.id)
645
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
646
                self.user.renew_token()
647
            #self.user.flush_sessions()
648
            if not self.user.has_auth_provider('local'):
649
                self.user.add_auth_provider('local', auth_backend='astakos')
650

    
651
        except BaseException, e:
652
            logger.exception(e)
653
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
654

    
655

    
656

    
657

    
658
app_name_label       =  "Project name"
659
app_name_placeholder = _("myproject.mylab.ntua.gr")
660
app_name_validator   =  validators.RegexValidator(
661
                            DOMAIN_VALUE_REGEX,
662
                            _(astakos_messages.DOMAIN_VALUE_ERR),
663
                            'invalid')
664
app_name_help        =  _("""
665
        The Project's name should be in a domain format.
666
        The domain shouldn't neccessarily exist in the real
667
        world but is helpful to imply a structure.
668
        e.g.: myproject.mylab.ntua.gr or
669
        myservice.myteam.myorganization""")
670
app_name_widget      =  forms.TextInput(
671
                            attrs={'placeholder': app_name_placeholder})
672

    
673

    
674
app_home_label       =  "Homepage URL"
675
app_home_placeholder =  'myinstitution.org/myproject/'
676
app_home_help        =  _("""
677
        URL pointing at your project's site.
678
        e.g.: myinstitution.org/myproject/.
679
        Leave blank if there is no website.""")
680
app_home_widget      =  forms.TextInput(
681
                            attrs={'placeholder': app_home_placeholder})
682

    
683
app_desc_label       =  _("Description")
684
app_desc_help        =  _("""
685
        Please provide a short but descriptive abstract of your Project,
686
        so that anyone searching can quickly understand
687
        what this Project is about.""")
688

    
689
app_comment_label    =  _("Comments for review (private)")
690
app_comment_help     =  _("""
691
        Write down any comments you may have for the reviewer
692
        of this application (e.g. background and rationale to
693
        support your request).
694
        The comments are strictly for the review process
695
        and will not be published.""")
696

    
697
app_start_date_label =  _("Start date")
698
app_start_date_help  =  _("""
699
        Provide a date when your need your project to be created,
700
        and members to be able to join and get resources.
701
        This date is only a hint to help prioritize reviews.""")
702

    
703
app_end_date_label   =  _("Termination date")
704
app_end_date_help    =  _("""
705
        At this date, the project will be automatically terminated
706
        and its resource grants revoked from all members.
707
        Unless you know otherwise,
708
        it is best to start with a conservative estimation.
709
        You can always re-apply for an extension, if you need.""")
710

    
711
join_policy_label    =  _("Joining policy")
712
app_member_join_policy_help    =  _("""
713
        Text fo member_join_policy.""")
714
leave_policy_label   =  _("Leaving policy")
715
app_member_leave_policy_help    =  _("""
716
        Text fo member_leave_policy.""")
717

    
718
max_members_label    =  _("Maximum member count")
719
max_members_help     =  _("""
720
        Specify the maximum number of members this project may have,
721
        including the owner. Beyond this number, no new members
722
        may join the project and be granted the project resources.
723
        Unless you certainly for otherwise,
724
        it is best to start with a conservative limit.
725
        You can always request a raise when you need it.""")
726

    
727
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
728
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
729

    
730
class ProjectApplicationForm(forms.ModelForm):
731

    
732
    name = forms.CharField(
733
        label     = app_name_label,
734
        help_text = app_name_help,
735
        widget    = app_name_widget,
736
        validators = [app_name_validator])
737

    
738
    homepage = forms.URLField(
739
        label     = app_home_label,
740
        help_text = app_home_help,
741
        widget    = app_home_widget,
742
        required  = False)
743

    
744
    description = forms.CharField(
745
        label     = app_desc_label,
746
        help_text = app_desc_help,
747
        widget    = forms.Textarea,
748
        required  = False)
749

    
750
    comments = forms.CharField(
751
        label     = app_comment_label,
752
        help_text = app_comment_help,
753
        widget    = forms.Textarea,
754
        required  = False)
755

    
756
    start_date = forms.DateTimeField(
757
        label     = app_start_date_label,
758
        help_text = app_start_date_help,
759
        required  = False)
760

    
761
    end_date = forms.DateTimeField(
762
        label     = app_end_date_label,
763
        help_text = app_end_date_help)
764

    
765
    member_join_policy  = forms.TypedChoiceField(
766
        label     = join_policy_label,
767
        help_text = app_member_join_policy_help,
768
        initial   = 2,
769
        coerce    = int,
770
        choices   = join_policies)
771

    
772
    member_leave_policy = forms.TypedChoiceField(
773
        label     = leave_policy_label,
774
        help_text = app_member_leave_policy_help,
775
        coerce    = int,
776
        choices   = leave_policies)
777

    
778
    limit_on_members_number = forms.IntegerField(
779
        label     = max_members_label,
780
        help_text = max_members_help,
781
        required  = False)
782

    
783
    class Meta:
784
        model = ProjectApplication
785
        fields = ( 'name', 'homepage', 'description',
786
                    'start_date', 'end_date', 'comments',
787
                    'member_join_policy', 'member_leave_policy',
788
                    'limit_on_members_number')
789

    
790
    def __init__(self, *args, **kwargs):
791
        instance = kwargs.get('instance')
792
        self.precursor_application = instance
793
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
794
        # in case of new application remove closed join policy
795
        if not instance:
796
            policies = PROJECT_MEMBER_JOIN_POLICIES.copy()
797
            policies.pop('3')
798
            self.fields['member_join_policy'].choices = policies.iteritems()
799

    
800
    def clean_start_date(self):
801
        start_date = self.cleaned_data.get('start_date')
802
        if not self.precursor_application:
803
            today = datetime.now()
804
            today = datetime(today.year, today.month, today.day)
805
            if start_date and (start_date - today).days < 0:
806
                raise forms.ValidationError(
807
                _(astakos_messages.INVALID_PROJECT_START_DATE))
808
        return start_date
809

    
810
    def clean_end_date(self):
811
        start_date = self.cleaned_data.get('start_date')
812
        end_date = self.cleaned_data.get('end_date')
813
        today = datetime.now()
814
        today = datetime(today.year, today.month, today.day)
815
        if end_date and (end_date - today).days < 0:
816
            raise forms.ValidationError(
817
                _(astakos_messages.INVALID_PROJECT_END_DATE))
818
        if start_date and (end_date - start_date).days <= 0:
819
            raise forms.ValidationError(
820
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
821
        return end_date
822

    
823
    def clean(self):
824
        userid = self.data.get('user', None)
825
        self.user = None
826
        if userid:
827
            try:
828
                self.user = AstakosUser.objects.get(id=userid)
829
            except AstakosUser.DoesNotExist:
830
                pass
831
        if not self.user:
832
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
833
        super(ProjectApplicationForm, self).clean()
834
        return self.cleaned_data
835

    
836
    @property
837
    def resource_policies(self):
838
        policies = []
839
        append = policies.append
840
        for name, value in self.data.iteritems():
841
            if not value:
842
                continue
843
            uplimit = value
844
            if name.endswith('_uplimit'):
845
                subs = name.split('_uplimit')
846
                prefix, suffix = subs
847
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
848
                resource = Resource.objects.get(service__name=s, name=r)
849

    
850
                # keep only resource limits for selected resource groups
851
                if self.data.get(
852
                    'is_selected_%s' % resource.group, "0"
853
                 ) == "1":
854
                    d = model_to_dict(resource)
855
                    if uplimit:
856
                        d.update(dict(service=s, resource=r, uplimit=uplimit))
857
                    else:
858
                        d.update(dict(service=s, resource=r, uplimit=None))
859
                    append(d)
860

    
861
        return policies
862

    
863
    def save(self, commit=True):
864
        data = dict(self.cleaned_data)
865
        data['precursor_application'] = self.instance.id
866
        data['owner'] = self.user
867
        data['resource_policies'] = self.resource_policies
868
        submit_application(data, request_user=self.user)
869

    
870
class ProjectSortForm(forms.Form):
871
    sorting = forms.ChoiceField(
872
        label='Sort by',
873
        choices=(('name', 'Sort by Name'),
874
                 ('issue_date', 'Sort by Issue date'),
875
                 ('start_date', 'Sort by Start Date'),
876
                 ('end_date', 'Sort by End Date'),
877
#                  ('approved_members_num', 'Sort by Participants'),
878
                 ('state', 'Sort by Status'),
879
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
880
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
881
                 ('-name', 'Sort by Name'),
882
                 ('-issue_date', 'Sort by Issue date'),
883
                 ('-start_date', 'Sort by Start Date'),
884
                 ('-end_date', 'Sort by End Date'),
885
#                  ('-approved_members_num', 'Sort by Participants'),
886
                 ('-state', 'Sort by Status'),
887
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
888
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
889
        ),
890
        required=True
891
    )
892

    
893
class AddProjectMembersForm(forms.Form):
894
    q = forms.CharField(
895
        max_length=800, widget=forms.Textarea, label=_('Add members'),
896
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
897

    
898
    def __init__(self, *args, **kwargs):
899
        chain_id = kwargs.pop('chain_id', None)
900
        if chain_id:
901
            self.project = Project.objects.get(id=chain_id)
902
        self.request_user = kwargs.pop('request_user', None)
903
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
904

    
905
    def clean(self):
906
        try:
907
            accept_membership_checks(self.project, self.request_user)
908
        except PermissionDenied, e:
909
            raise forms.ValidationError(e)
910

    
911
        q = self.cleaned_data.get('q') or ''
912
        users = q.split(',')
913
        users = list(u.strip() for u in users if u)
914
        db_entries = AstakosUser.objects.filter(email__in=users)
915
        unknown = list(set(users) - set(u.email for u in db_entries))
916
        if unknown:
917
            raise forms.ValidationError(
918
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
919
        self.valid_users = db_entries
920
        return self.cleaned_data
921

    
922
    def get_valid_users(self):
923
        """Should be called after form cleaning"""
924
        try:
925
            return self.valid_users
926
        except:
927
            return ()
928

    
929
class ProjectMembersSortForm(forms.Form):
930
    sorting = forms.ChoiceField(
931
        label='Sort by',
932
        choices=(('person__email', 'User Id'),
933
                 ('person__first_name', 'Name'),
934
                 ('acceptance_date', 'Acceptance date')
935
        ),
936
        required=True
937
    )
938

    
939

    
940
class ProjectSearchForm(forms.Form):
941
    q = forms.CharField(max_length=200, label='Search project', required=False)
942

    
943

    
944
class ExtendedProfileForm(ProfileForm):
945
    """
946
    Profile form that combines `email change` and `password change` user
947
    actions by propagating submited data to internal EmailChangeForm
948
    and ExtendedPasswordChangeForm objects.
949
    """
950

    
951
    password_change_form = None
952
    email_change_form = None
953

    
954
    password_change = False
955
    email_change = False
956

    
957
    extra_forms_fields = {
958
        'email': ['new_email_address'],
959
        'password': ['old_password', 'new_password1', 'new_password2']
960
    }
961

    
962
    fields = ('email')
963
    change_password = forms.BooleanField(initial=False, required=False)
964
    change_email = forms.BooleanField(initial=False, required=False)
965

    
966
    email_changed = False
967
    password_changed = False
968

    
969
    def __init__(self, *args, **kwargs):
970
        session_key = kwargs.get('session_key', None)
971
        self.fields_list = [
972
                'email',
973
                'new_email_address',
974
                'first_name',
975
                'last_name',
976
                'auth_token',
977
                'auth_token_expires',
978
                'old_password',
979
                'new_password1',
980
                'new_password2',
981
                'change_email',
982
                'change_password',
983
        ]
984

    
985
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
986
        self.session_key = session_key
987
        if self.instance.can_change_password():
988
            self.password_change = True
989
        else:
990
            self.fields_list.remove('old_password')
991
            self.fields_list.remove('new_password1')
992
            self.fields_list.remove('new_password2')
993
            self.fields_list.remove('change_password')
994
            del self.fields['change_password']
995

    
996

    
997
        if EMAILCHANGE_ENABLED and self.instance.can_change_email():
998
            self.email_change = True
999
        else:
1000
            self.fields_list.remove('new_email_address')
1001
            self.fields_list.remove('change_email')
1002
            del self.fields['change_email']
1003

    
1004
        self._init_extra_forms()
1005
        self.save_extra_forms = []
1006
        self.success_messages = []
1007
        self.fields.keyOrder = self.fields_list
1008

    
1009

    
1010
    def _init_extra_form_fields(self):
1011
        if self.email_change:
1012
            self.fields.update(self.email_change_form.fields)
1013
            self.fields['new_email_address'].required = False
1014

    
1015
        if self.password_change:
1016
            self.fields.update(self.password_change_form.fields)
1017
            self.fields['old_password'].required = False
1018
            self.fields['old_password'].label = _('Password')
1019
            self.fields['old_password'].initial = 'password'
1020
            self.fields['new_password1'].required = False
1021
            self.fields['new_password2'].required = False
1022

    
1023
    def _update_extra_form_errors(self):
1024
        if self.cleaned_data.get('change_password'):
1025
            self.errors.update(self.password_change_form.errors)
1026
        if self.cleaned_data.get('change_email'):
1027
            self.errors.update(self.email_change_form.errors)
1028

    
1029
    def _init_extra_forms(self):
1030
        self.email_change_form = EmailChangeForm(self.data)
1031
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1032
                                   data=self.data, session_key=self.session_key)
1033
        self._init_extra_form_fields()
1034

    
1035
    def is_valid(self):
1036
        password, email = True, True
1037
        profile = super(ExtendedProfileForm, self).is_valid()
1038
        if profile and self.cleaned_data.get('change_password', None):
1039

    
1040
            password = self.password_change_form.is_valid()
1041
            self.save_extra_forms.append('password')
1042
        if profile and self.cleaned_data.get('change_email'):
1043
            self.fields['new_email_address'].required = True
1044
            email = self.email_change_form.is_valid()
1045
            self.save_extra_forms.append('email')
1046

    
1047
        if not password or not email:
1048
            self._update_extra_form_errors()
1049

    
1050
        return all([profile, password, email])
1051

    
1052
    def save(self, request, *args, **kwargs):
1053
        if 'email' in self.save_extra_forms:
1054
            self.email_change_form.save(request, *args, **kwargs)
1055
            self.email_changed = True
1056
        if 'password' in self.save_extra_forms:
1057
            self.password_change_form.save(*args, **kwargs)
1058
            self.password_changed = True
1059
        return super(ExtendedProfileForm, self).save(*args, **kwargs)
1060