Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 37d59b27

History | View | Annotate | Download (40.4 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,
61
    ProjectApplication, Project)
62
from astakos.im.settings import (
63
    INVITATIONS_PER_LEVEL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY,
64
    RECAPTCHA_ENABLED, 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
    )
69
from astakos.im import presentation
70
from astakos.im.widgets import DummyWidget, RecaptchaWidget
71
from astakos.im.functions import (
72
    send_change_email, submit_application, accept_membership_checks)
73

    
74
from astakos.im.util import reserved_email, reserved_verified_email, \
75
                            get_query, model_to_dict
76
from astakos.im import auth_providers
77

    
78
import astakos.im.messages as astakos_messages
79

    
80
import logging
81
import hashlib
82
import recaptcha.client.captcha as captcha
83
import re
84

    
85
logger = logging.getLogger(__name__)
86

    
87
DOMAIN_VALUE_REGEX = re.compile(
88
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
89
    re.IGNORECASE)
90

    
91
class StoreUserMixin(object):
92

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

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

    
108

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
205

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

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

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

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

    
233

    
234
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
235
    id = forms.CharField(
236
        widget=forms.HiddenInput(),
237
        label='',
238
        required=False
239
    )
240
    third_party_identifier = forms.CharField(
241
        widget=forms.HiddenInput(),
242
        label=''
243
    )
244
    email = forms.EmailField(
245
        label='Contact email',
246
        help_text = 'This is needed for contact purposes. ' \
247
        'It doesn&#39;t need to be the same with the one you ' \
248
        'provided to login previously. '
249
    )
250

    
251
    class Meta:
252
        model = AstakosUser
253
        fields = ['id', 'email', 'third_party_identifier',
254
                  'first_name', 'last_name', 'has_signed_terms']
255

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

    
264
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
265

    
266
        if not get_latest_terms():
267
            del self.fields['has_signed_terms']
268

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

    
277
    def clean_email(self):
278
        email = self.cleaned_data['email']
279
        if not email:
280
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
281
        if reserved_verified_email(email):
282
            provider_id = self.request.REQUEST.get('provider', 'local')
283
            provider = auth_providers.get_provider(provider_id)
284
            extra_message = provider.get_add_to_existing_account_msg
285

    
286
            raise forms.ValidationError(mark_safe(_(astakos_messages.EMAIL_USED) + ' ' +
287
                                        extra_message))
288
        return email
289

    
290
    def clean_has_signed_terms(self):
291
        has_signed_terms = self.cleaned_data['has_signed_terms']
292
        if not has_signed_terms:
293
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
294
        return has_signed_terms
295

    
296
    def post_store_user(self, user, request):
297
        pending = PendingThirdPartyUser.objects.get(
298
            token=request.POST.get('third_party_token'),
299
            third_party_identifier=
300
            self.cleaned_data.get('third_party_identifier'))
301
        provider = pending.get_provider(user)
302
        provider.add_to_user()
303
        pending.delete()
304

    
305
    def save(self, commit=True):
306
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
307
        user.set_unusable_password()
308
        user.renew_token()
309
        if commit:
310
            user.save()
311
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
312
        return user
313

    
314

    
315
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
316
    """
317
    Extends the ThirdPartyUserCreationForm: email is readonly.
318
    """
319
    def __init__(self, *args, **kwargs):
320
        """
321
        Changes the order of fields, and removes the username field.
322
        """
323
        super(
324
            InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
325

    
326
        #set readonly form fields
327
        ro = ('email',)
328
        for f in ro:
329
            self.fields[f].widget.attrs['readonly'] = True
330

    
331
    def save(self, commit=True):
332
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
333
        user.set_invitation_level()
334
        user.email_verified = True
335
        if commit:
336
            user.save()
337
        return user
338

    
339

    
340
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
341
    additional_email = forms.CharField(
342
        widget=forms.HiddenInput(), label='', required=False)
343

    
344
    def __init__(self, *args, **kwargs):
345
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
346
        # copy email value to additional_mail in case user will change it
347
        name = 'email'
348
        field = self.fields[name]
349
        self.initial['additional_email'] = self.initial.get(name, field.initial)
350
        self.initial['email'] = None
351

    
352

    
353
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
354
                                        InvitedThirdPartyUserCreationForm):
355
    pass
356

    
357

    
358
class LoginForm(AuthenticationForm):
359
    username = forms.EmailField(label=_("Email"))
360
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
361
    recaptcha_response_field = forms.CharField(
362
        widget=RecaptchaWidget, label='')
363

    
364
    def __init__(self, *args, **kwargs):
365
        was_limited = kwargs.get('was_limited', False)
366
        request = kwargs.get('request', None)
367
        if request:
368
            self.ip = request.META.get('REMOTE_ADDR',
369
                                       request.META.get('HTTP_X_REAL_IP', None))
370

    
371
        t = ('request', 'was_limited')
372
        for elem in t:
373
            if elem in kwargs.keys():
374
                kwargs.pop(elem)
375
        super(LoginForm, self).__init__(*args, **kwargs)
376

    
377
        self.fields.keyOrder = ['username', 'password']
378
        if was_limited and RECAPTCHA_ENABLED:
379
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
380
                                         'recaptcha_response_field', ])
381

    
382
    def clean_username(self):
383
        return self.cleaned_data['username'].lower()
384

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

    
390
    def clean_recaptcha_challenge_field(self):
391
        if 'recaptcha_response_field' in self.cleaned_data:
392
            self.validate_captcha()
393
        return self.cleaned_data['recaptcha_challenge_field']
394

    
395
    def validate_captcha(self):
396
        rcf = self.cleaned_data['recaptcha_challenge_field']
397
        rrf = self.cleaned_data['recaptcha_response_field']
398
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
399
        if not check.is_valid:
400
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
401

    
402
    def clean(self):
403
        """
404
        Override default behavior in order to check user's activation later
405
        """
406
        username = self.cleaned_data.get('username')
407

    
408
        if username:
409
            try:
410
                user = AstakosUser.objects.get_by_identifier(username)
411
                if not user.has_auth_provider('local'):
412
                    provider = auth_providers.get_provider('local', user)
413
                    raise forms.ValidationError(
414
                        provider.get_login_disabled_msg)
415
            except AstakosUser.DoesNotExist:
416
                pass
417

    
418
        try:
419
            super(LoginForm, self).clean()
420
        except forms.ValidationError, e:
421
            if self.user_cache is None:
422
                raise
423
            if not self.user_cache.is_active:
424
                msg = self.user_cache.get_inactive_message('local')
425
                raise forms.ValidationError(msg)
426
            if self.request:
427
                if not self.request.session.test_cookie_worked():
428
                    raise
429
        return self.cleaned_data
430

    
431

    
432
class ProfileForm(forms.ModelForm):
433
    """
434
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
435
    Most of the fields are readonly since the user is not allowed to change
436
    them.
437

438
    The class defines a save method which sets ``is_verified`` to True so as the
439
    user during the next login will not to be redirected to profile page.
440
    """
441
    email = forms.EmailField(label='E-mail address', help_text='E-mail address')
442
    renew = forms.BooleanField(label='Renew token', required=False)
443
    uuid = forms.CharField(label='User id', required=False)
444

    
445
    class Meta:
446
        model = AstakosUser
447
        fields = ('email', 'first_name', 'last_name', 'auth_token',
448
                  'auth_token_expires', 'uuid')
449

    
450
    def __init__(self, *args, **kwargs):
451
        self.session_key = kwargs.pop('session_key', None)
452
        super(ProfileForm, self).__init__(*args, **kwargs)
453
        instance = getattr(self, 'instance', None)
454
        ro_fields = ('email', 'auth_token', 'auth_token_expires', 'uuid')
455
        if instance and instance.id:
456
            for field in ro_fields:
457
                self.fields[field].widget.attrs['readonly'] = True
458

    
459
    def clean_email(self):
460
        return self.instance.email
461

    
462
    def clean_auth_token(self):
463
        return self.instance.auth_token
464

    
465
    def clean_auth_token_expires(self):
466
        return self.instance.auth_token_expires
467

    
468
    def clean_uuid(self):
469
        return self.instance.uuid
470

    
471
    def save(self, commit=True):
472
        user = super(ProfileForm, self).save(commit=False)
473
        user.is_verified = True
474
        if self.cleaned_data.get('renew'):
475
            user.renew_token(
476
                flush_sessions=True,
477
                current_key=self.session_key
478
            )
479
        if commit:
480
            user.save()
481
        return user
482

    
483

    
484

    
485
class FeedbackForm(forms.Form):
486
    """
487
    Form for writing feedback.
488
    """
489
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
490
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
491
                                    required=False)
492

    
493

    
494
class SendInvitationForm(forms.Form):
495
    """
496
    Form for sending an invitations
497
    """
498

    
499
    email = forms.EmailField(required=True, label='Email address')
500
    first_name = forms.EmailField(label='First name')
501
    last_name = forms.EmailField(label='Last name')
502

    
503

    
504
class ExtendedPasswordResetForm(PasswordResetForm):
505
    """
506
    Extends PasswordResetForm by overriding
507

508
    save method: to pass a custom from_email in send_mail.
509
    clean_email: to handle local auth provider checks
510
    """
511
    def clean_email(self):
512
        # we override the default django auth clean_email to provide more
513
        # detailed messages in case of inactive users
514
        email = self.cleaned_data['email']
515
        try:
516
            user = AstakosUser.objects.get_by_identifier(email)
517
            self.users_cache = [user]
518
            if not user.is_active:
519
                raise forms.ValidationError(user.get_inactive_message('local'))
520

    
521
            provider = auth_providers.get_provider('local', user)
522
            if not user.has_usable_password():
523
                msg = provider.get_unusable_password_msg
524
                raise forms.ValidationError(msg)
525

    
526
            if not user.can_change_password():
527
                msg = provider.get_cannot_change_password_msg
528
                raise forms.ValidationError(msg)
529

    
530
        except AstakosUser.DoesNotExist:
531
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
532
        return email
533

    
534
    def save(
535
        self, domain_override=None, email_template_name='registration/password_reset_email.html',
536
            use_https=False, token_generator=default_token_generator, request=None):
537
        """
538
        Generates a one-use only link for resetting password and sends to the user.
539
        """
540
        for user in self.users_cache:
541
            url = user.astakosuser.get_password_reset_url(token_generator)
542
            url = urljoin(BASEURL, url)
543
            t = loader.get_template(email_template_name)
544
            c = {
545
                'email': user.email,
546
                'url': url,
547
                'site_name': SITENAME,
548
                'user': user,
549
                'baseurl': BASEURL,
550
                'support': CONTACT_EMAIL
551
            }
552
            from_email = settings.SERVER_EMAIL
553
            send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
554
                      t.render(Context(c)),
555
                      from_email,
556
                      [user.email],
557
                      connection=get_connection())
558

    
559

    
560
class EmailChangeForm(forms.ModelForm):
561

    
562
    class Meta:
563
        model = EmailChange
564
        fields = ('new_email_address',)
565

    
566
    def clean_new_email_address(self):
567
        addr = self.cleaned_data['new_email_address']
568
        if reserved_verified_email(addr):
569
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
570
        return addr
571

    
572
    def save(self, request, email_template_name='registration/email_change_email.txt', commit=True):
573
        ec = super(EmailChangeForm, self).save(commit=False)
574
        ec.user = request.user
575
        # delete pending email changes
576
        request.user.emailchanges.all().delete()
577

    
578
        activation_key = hashlib.sha1(
579
            str(random()) + smart_str(ec.new_email_address))
580
        ec.activation_key = activation_key.hexdigest()
581
        if commit:
582
            ec.save()
583
        send_change_email(ec, request, email_template_name=email_template_name)
584

    
585

    
586
class SignApprovalTermsForm(forms.ModelForm):
587

    
588
    class Meta:
589
        model = AstakosUser
590
        fields = ("has_signed_terms",)
591

    
592
    def __init__(self, *args, **kwargs):
593
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
594

    
595
    def clean_has_signed_terms(self):
596
        has_signed_terms = self.cleaned_data['has_signed_terms']
597
        if not has_signed_terms:
598
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
599
        return has_signed_terms
600

    
601

    
602
class InvitationForm(forms.ModelForm):
603

    
604
    username = forms.EmailField(label=_("Email"))
605

    
606
    def __init__(self, *args, **kwargs):
607
        super(InvitationForm, self).__init__(*args, **kwargs)
608

    
609
    class Meta:
610
        model = Invitation
611
        fields = ('username', 'realname')
612

    
613
    def clean_username(self):
614
        username = self.cleaned_data['username']
615
        try:
616
            Invitation.objects.get(username=username)
617
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
618
        except Invitation.DoesNotExist:
619
            pass
620
        return username
621

    
622

    
623
class ExtendedPasswordChangeForm(PasswordChangeForm):
624
    """
625
    Extends PasswordChangeForm by enabling user
626
    to optionally renew also the token.
627
    """
628
    if not NEWPASSWD_INVALIDATE_TOKEN:
629
        renew = forms.BooleanField(label='Renew token', required=False,
630
                                   initial=True,
631
                                   help_text='Unsetting this may result in security risk.')
632

    
633
    def __init__(self, user, *args, **kwargs):
634
        self.session_key = kwargs.pop('session_key', None)
635
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
636

    
637
    def save(self, commit=True):
638
        try:
639
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
640
                self.user.renew_token()
641
            self.user.flush_sessions(current_key=self.session_key)
642
        except AttributeError:
643
            # if user model does has not such methods
644
            pass
645
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
646

    
647
class ExtendedSetPasswordForm(SetPasswordForm):
648
    """
649
    Extends SetPasswordForm by enabling user
650
    to optionally renew also the token.
651
    """
652
    if not NEWPASSWD_INVALIDATE_TOKEN:
653
        renew = forms.BooleanField(
654
            label='Renew token',
655
            required=False,
656
            initial=True,
657
            help_text='Unsetting this may result in security risk.')
658

    
659
    def __init__(self, user, *args, **kwargs):
660
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
661

    
662
    @transaction.commit_on_success()
663
    def save(self, commit=True):
664
        try:
665
            self.user = AstakosUser.objects.get(id=self.user.id)
666
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
667
                self.user.renew_token()
668

    
669
            provider = auth_providers.get_provider('local', self.user)
670
            if provider.get_add_policy:
671
                provider.add_to_user()
672

    
673
        except BaseException, e:
674
            logger.exception(e)
675
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
676

    
677

    
678

    
679

    
680
app_name_label       =  "Project name"
681
app_name_placeholder = _("myproject.mylab.ntua.gr")
682
app_name_validator   =  validators.RegexValidator(
683
                            DOMAIN_VALUE_REGEX,
684
                            _(astakos_messages.DOMAIN_VALUE_ERR),
685
                            'invalid')
686
app_name_help        =  _("""
687
        The project's name should be in a domain format.
688
        The domain shouldn't neccessarily exist in the real
689
        world but is helpful to imply a structure.
690
        e.g.: myproject.mylab.ntua.gr or
691
        myservice.myteam.myorganization""")
692
app_name_widget      =  forms.TextInput(
693
                            attrs={'placeholder': app_name_placeholder})
694

    
695

    
696
app_home_label       =  "Homepage URL"
697
app_home_placeholder =  'myinstitution.org/myproject/'
698
app_home_help        =  _("""
699
        URL pointing at your project's site.
700
        e.g.: myinstitution.org/myproject/.
701
        Leave blank if there is no website.""")
702
app_home_widget      =  forms.TextInput(
703
                            attrs={'placeholder': app_home_placeholder})
704

    
705
app_desc_label       =  _("Description")
706
app_desc_help        =  _("""
707
        Please provide a short but descriptive abstract of your
708
        project, so that anyone searching can quickly understand
709
        what this project is about.""")
710

    
711
app_comment_label    =  _("Comments for review (private)")
712
app_comment_help     =  _("""
713
        Write down any comments you may have for the reviewer
714
        of this application (e.g. background and rationale to
715
        support your request).
716
        The comments are strictly for the review process
717
        and will not be made public.""")
718

    
719
app_start_date_label =  _("Start date")
720
app_start_date_help  =  _("""
721
        Provide a date when your need your project to be created,
722
        and members to be able to join and get resources.
723
        This date is only a hint to help prioritize reviews.""")
724

    
725
app_end_date_label   =  _("Termination date")
726
app_end_date_help    =  _("""
727
        At this date, the project will be automatically terminated
728
        and its resource grants revoked from all members. If you are
729
        not certain, it is best to start with a conservative estimation.
730
        You can always re-apply for an extension, if you need.""")
731

    
732
join_policy_label    =  _("Joining policy")
733
app_member_join_policy_help    =  _("""
734
        Select how new members are accepted into the project.""")
735
leave_policy_label   =  _("Leaving policy")
736
app_member_leave_policy_help    =  _("""
737
        Select how new members can leave the project.""")
738

    
739
max_members_label    =  _("Maximum member count")
740
max_members_help     =  _("""
741
        Specify the maximum number of members this project may have,
742
        including the owner. Beyond this number, no new members
743
        may join the project and be granted the project resources.
744
        If you are not certain, it is best to start with a conservative
745
        limit. You can always request a raise when you need it.""")
746

    
747
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
748
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
749

    
750
class ProjectApplicationForm(forms.ModelForm):
751

    
752
    name = forms.CharField(
753
        label     = app_name_label,
754
        help_text = app_name_help,
755
        widget    = app_name_widget,
756
        validators = [app_name_validator])
757

    
758
    homepage = forms.URLField(
759
        label     = app_home_label,
760
        help_text = app_home_help,
761
        widget    = app_home_widget,
762
        required  = False)
763

    
764
    description = forms.CharField(
765
        label     = app_desc_label,
766
        help_text = app_desc_help,
767
        widget    = forms.Textarea,
768
        required  = False)
769

    
770
    comments = forms.CharField(
771
        label     = app_comment_label,
772
        help_text = app_comment_help,
773
        widget    = forms.Textarea,
774
        required  = False)
775

    
776
    start_date = forms.DateTimeField(
777
        label     = app_start_date_label,
778
        help_text = app_start_date_help,
779
        required  = False)
780

    
781
    end_date = forms.DateTimeField(
782
        label     = app_end_date_label,
783
        help_text = app_end_date_help)
784

    
785
    member_join_policy  = forms.TypedChoiceField(
786
        label     = join_policy_label,
787
        help_text = app_member_join_policy_help,
788
        initial   = 2,
789
        coerce    = int,
790
        choices   = join_policies)
791

    
792
    member_leave_policy = forms.TypedChoiceField(
793
        label     = leave_policy_label,
794
        help_text = app_member_leave_policy_help,
795
        coerce    = int,
796
        choices   = leave_policies)
797

    
798
    limit_on_members_number = forms.IntegerField(
799
        label     = max_members_label,
800
        help_text = max_members_help,
801
        min_value = 0,
802
        required  = False)
803

    
804
    class Meta:
805
        model = ProjectApplication
806
        fields = ( 'name', 'homepage', 'description',
807
                    'start_date', 'end_date', 'comments',
808
                    'member_join_policy', 'member_leave_policy',
809
                    'limit_on_members_number')
810

    
811
    def __init__(self, *args, **kwargs):
812
        instance = kwargs.get('instance')
813
        self.precursor_application = instance
814
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
815
        # in case of new application remove closed join policy
816
        if not instance:
817
            policies = PROJECT_MEMBER_JOIN_POLICIES.copy()
818
            policies.pop('3')
819
            self.fields['member_join_policy'].choices = policies.iteritems()
820

    
821
    def clean_start_date(self):
822
        start_date = self.cleaned_data.get('start_date')
823
        if not self.precursor_application:
824
            today = datetime.now()
825
            today = datetime(today.year, today.month, today.day)
826
            if start_date and (start_date - today).days < 0:
827
                raise forms.ValidationError(
828
                _(astakos_messages.INVALID_PROJECT_START_DATE))
829
        return start_date
830

    
831
    def clean_end_date(self):
832
        start_date = self.cleaned_data.get('start_date')
833
        end_date = self.cleaned_data.get('end_date')
834
        today = datetime.now()
835
        today = datetime(today.year, today.month, today.day)
836
        if end_date and (end_date - today).days < 0:
837
            raise forms.ValidationError(
838
                _(astakos_messages.INVALID_PROJECT_END_DATE))
839
        if start_date and (end_date - start_date).days <= 0:
840
            raise forms.ValidationError(
841
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
842
        return end_date
843

    
844
    def clean(self):
845
        userid = self.data.get('user', None)
846
        policies = self.resource_policies
847
        self.user = None
848
        if userid:
849
            try:
850
                self.user = AstakosUser.objects.get(id=userid)
851
            except AstakosUser.DoesNotExist:
852
                pass
853
        if not self.user:
854
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
855
        super(ProjectApplicationForm, self).clean()
856
        return self.cleaned_data
857

    
858
    @property
859
    def resource_policies(self):
860
        policies = []
861
        append = policies.append
862
        for name, value in self.data.iteritems():
863
            if not value:
864
                continue
865
            uplimit = value
866
            if name.endswith('_uplimit'):
867
                subs = name.split('_uplimit')
868
                prefix, suffix = subs
869
                try:
870
                    resource = Resource.objects.get(name=prefix)
871
                except Resource.DoesNotExist:
872
                    raise forms.ValidationError("Resource %s does not exist" %
873
                                                resource.name)
874
                # keep only resource limits for selected resource groups
875
                if self.data.get(
876
                    'is_selected_%s' % resource.group, "0"
877
                 ) == "1":
878
                    if not resource.allow_in_projects:
879
                        raise forms.ValidationError("Invalid resource %s" %
880
                                                    resource.name)
881
                    d = model_to_dict(resource)
882
                    if uplimit:
883
                        d.update(dict(resource=prefix, uplimit=uplimit))
884
                    else:
885
                        d.update(dict(resource=prefix, uplimit=None))
886
                    append(d)
887

    
888
        ordered_keys = presentation.RESOURCES['resources_order']
889
        def resource_order(r):
890
            if r['str_repr'] in ordered_keys:
891
                return ordered_keys.index(r['str_repr'])
892
            else:
893
                return -1
894

    
895
        policies = sorted(policies, key=resource_order)
896
        return policies
897

    
898
    def save(self, commit=True):
899
        data = dict(self.cleaned_data)
900
        data['precursor_application'] = self.instance.id
901
        is_new = self.instance.id is None
902
        data['owner'] = self.user if is_new else self.instance.owner
903
        data['resource_policies'] = self.resource_policies
904
        submit_application(data, request_user=self.user)
905

    
906
class ProjectSortForm(forms.Form):
907
    sorting = forms.ChoiceField(
908
        label='Sort by',
909
        choices=(('name', 'Sort by Name'),
910
                 ('issue_date', 'Sort by Issue date'),
911
                 ('start_date', 'Sort by Start Date'),
912
                 ('end_date', 'Sort by End Date'),
913
#                  ('approved_members_num', 'Sort by Participants'),
914
                 ('state', 'Sort by Status'),
915
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
916
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
917
                 ('-name', 'Sort by Name'),
918
                 ('-issue_date', 'Sort by Issue date'),
919
                 ('-start_date', 'Sort by Start Date'),
920
                 ('-end_date', 'Sort by End Date'),
921
#                  ('-approved_members_num', 'Sort by Participants'),
922
                 ('-state', 'Sort by Status'),
923
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
924
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
925
        ),
926
        required=True
927
    )
928

    
929
class AddProjectMembersForm(forms.Form):
930
    q = forms.CharField(
931
        max_length=800, widget=forms.Textarea, label=_('Add members'),
932
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
933

    
934
    def __init__(self, *args, **kwargs):
935
        chain_id = kwargs.pop('chain_id', None)
936
        if chain_id:
937
            self.project = Project.objects.get(id=chain_id)
938
        self.request_user = kwargs.pop('request_user', None)
939
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
940

    
941
    def clean(self):
942
        try:
943
            accept_membership_checks(self.project, self.request_user)
944
        except PermissionDenied, e:
945
            raise forms.ValidationError(e)
946

    
947
        q = self.cleaned_data.get('q') or ''
948
        users = q.split(',')
949
        users = list(u.strip() for u in users if u)
950
        db_entries = AstakosUser.objects.verified().filter(email__in=users)
951
        unknown = list(set(users) - set(u.email for u in db_entries))
952
        if unknown:
953
            raise forms.ValidationError(
954
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
955
        self.valid_users = db_entries
956
        return self.cleaned_data
957

    
958
    def get_valid_users(self):
959
        """Should be called after form cleaning"""
960
        try:
961
            return self.valid_users
962
        except:
963
            return ()
964

    
965
class ProjectMembersSortForm(forms.Form):
966
    sorting = forms.ChoiceField(
967
        label='Sort by',
968
        choices=(('person__email', 'User Id'),
969
                 ('person__first_name', 'Name'),
970
                 ('acceptance_date', 'Acceptance date')
971
        ),
972
        required=True
973
    )
974

    
975

    
976
class ProjectSearchForm(forms.Form):
977
    q = forms.CharField(max_length=200, label='Search project', required=False)
978

    
979

    
980
class ExtendedProfileForm(ProfileForm):
981
    """
982
    Profile form that combines `email change` and `password change` user
983
    actions by propagating submited data to internal EmailChangeForm
984
    and ExtendedPasswordChangeForm objects.
985
    """
986

    
987
    password_change_form = None
988
    email_change_form = None
989

    
990
    password_change = False
991
    email_change = False
992

    
993
    extra_forms_fields = {
994
        'email': ['new_email_address'],
995
        'password': ['old_password', 'new_password1', 'new_password2']
996
    }
997

    
998
    fields = ('email')
999
    change_password = forms.BooleanField(initial=False, required=False)
1000
    change_email = forms.BooleanField(initial=False, required=False)
1001

    
1002
    email_changed = False
1003
    password_changed = False
1004

    
1005
    def __init__(self, *args, **kwargs):
1006
        session_key = kwargs.get('session_key', None)
1007
        self.fields_list = [
1008
                'email',
1009
                'new_email_address',
1010
                'first_name',
1011
                'last_name',
1012
                'auth_token',
1013
                'auth_token_expires',
1014
                'old_password',
1015
                'new_password1',
1016
                'new_password2',
1017
                'change_email',
1018
                'change_password',
1019
                'uuid'
1020
        ]
1021

    
1022
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
1023
        self.session_key = session_key
1024
        if self.instance.can_change_password():
1025
            self.password_change = True
1026
        else:
1027
            self.fields_list.remove('old_password')
1028
            self.fields_list.remove('new_password1')
1029
            self.fields_list.remove('new_password2')
1030
            self.fields_list.remove('change_password')
1031
            del self.fields['change_password']
1032

    
1033
        if EMAILCHANGE_ENABLED and self.instance.can_change_email():
1034
            self.email_change = True
1035
        else:
1036
            self.fields_list.remove('new_email_address')
1037
            self.fields_list.remove('change_email')
1038
            del self.fields['change_email']
1039

    
1040
        self._init_extra_forms()
1041
        self.save_extra_forms = []
1042
        self.success_messages = []
1043
        self.fields.keyOrder = self.fields_list
1044

    
1045

    
1046
    def _init_extra_form_fields(self):
1047
        if self.email_change:
1048
            self.fields.update(self.email_change_form.fields)
1049
            self.fields['new_email_address'].required = False
1050
            self.fields['email'].help_text = _('Change the email associated with '
1051
                                               'your account. This email will '
1052
                                               'remain active until you verify '
1053
                                               'your new one.')
1054

    
1055
        if self.password_change:
1056
            self.fields.update(self.password_change_form.fields)
1057
            self.fields['old_password'].required = False
1058
            self.fields['old_password'].label = _('Password')
1059
            self.fields['old_password'].help_text = _('Change your password.')
1060
            self.fields['old_password'].initial = 'password'
1061
            self.fields['new_password1'].required = False
1062
            self.fields['new_password2'].required = False
1063

    
1064
    def _update_extra_form_errors(self):
1065
        if self.cleaned_data.get('change_password'):
1066
            self.errors.update(self.password_change_form.errors)
1067
        if self.cleaned_data.get('change_email'):
1068
            self.errors.update(self.email_change_form.errors)
1069

    
1070
    def _init_extra_forms(self):
1071
        self.email_change_form = EmailChangeForm(self.data)
1072
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1073
                                   data=self.data, session_key=self.session_key)
1074
        self._init_extra_form_fields()
1075

    
1076
    def is_valid(self):
1077
        password, email = True, True
1078
        profile = super(ExtendedProfileForm, self).is_valid()
1079
        if profile and self.cleaned_data.get('change_password', None):
1080

    
1081
            password = self.password_change_form.is_valid()
1082
            self.save_extra_forms.append('password')
1083
        if profile and self.cleaned_data.get('change_email'):
1084
            self.fields['new_email_address'].required = True
1085
            email = self.email_change_form.is_valid()
1086
            self.save_extra_forms.append('email')
1087

    
1088
        if not password or not email:
1089
            self._update_extra_form_errors()
1090

    
1091
        return all([profile, password, email])
1092

    
1093
    def save(self, request, *args, **kwargs):
1094
        if 'email' in self.save_extra_forms:
1095
            self.email_change_form.save(request, *args, **kwargs)
1096
            self.email_changed = True
1097
        if 'password' in self.save_extra_forms:
1098
            self.password_change_form.save(*args, **kwargs)
1099
            self.password_changed = True
1100
        return super(ExtendedProfileForm, self).save(*args, **kwargs)
1101