Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 251b83be

History | View | Annotate | Download (40.5 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 random import random
34
from datetime import datetime, timedelta
35

    
36
from django import forms
37
from django.utils.translation import ugettext as _
38
from django.contrib.auth.forms import (
39
    UserCreationForm, AuthenticationForm,
40
    PasswordResetForm, PasswordChangeForm,
41
    SetPasswordForm)
42
from django.core.mail import send_mail, get_connection
43
from django.contrib.auth.tokens import default_token_generator
44
from django.template import Context, loader
45
from django.utils.http import int_to_base36
46
from django.core.urlresolvers import reverse
47
from django.utils.safestring import mark_safe
48
from django.utils.encoding import smart_str
49
from django.conf import settings
50
from django.forms.models import fields_for_model
51
from django.db import transaction
52
from django.utils.encoding import smart_unicode
53
from django.core import validators
54
from django.contrib.auth.models import AnonymousUser
55
from django.core.exceptions import PermissionDenied
56

    
57
from synnefo.lib import join_urls
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, EMAILCHANGE_ENABLED,
67
    )
68
from astakos.im import presentation
69
from astakos.im.widgets import DummyWidget, RecaptchaWidget
70
from astakos.im.functions import (
71
    send_change_email, submit_application, accept_membership_checks)
72

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

    
77
import astakos.im.messages as astakos_messages
78

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

    
84
logger = logging.getLogger(__name__)
85

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

    
90
class StoreUserMixin(object):
91

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

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

    
107

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
204

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

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

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

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

    
232

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

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

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

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

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

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

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

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

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

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

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

    
313

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

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

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

    
338

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

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

    
351

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

    
356

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

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

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

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

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

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

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

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

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

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

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

    
430

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

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

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

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

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

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

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

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

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

    
482

    
483

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

    
492

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

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

    
502

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

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

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

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

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

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

    
558

    
559
class EmailChangeForm(forms.ModelForm):
560

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

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

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

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

    
584

    
585
class SignApprovalTermsForm(forms.ModelForm):
586

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

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

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

    
600

    
601
class InvitationForm(forms.ModelForm):
602

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

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

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

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

    
621

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

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

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

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

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

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

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

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

    
676

    
677

    
678

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

    
694

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

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

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

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

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

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

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

    
746
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
747
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
748

    
749
class ProjectApplicationForm(forms.ModelForm):
750

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
897
    def cleaned_resource_policies(self):
898
        return [(d['name'], d['uplimit']) for d in self.resource_policies]
899

    
900
    def save(self, commit=True):
901
        data = dict(self.cleaned_data)
902
        data['precursor_id'] = self.instance.id
903
        is_new = self.instance.id is None
904
        data['owner'] = self.user if is_new else self.instance.owner
905
        data['resource_policies'] = self.cleaned_resource_policies()
906
        data['request_user'] = self.user
907
        submit_application(**data)
908

    
909

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

    
933
class AddProjectMembersForm(forms.Form):
934
    q = forms.CharField(
935
        max_length=800, widget=forms.Textarea, label=_('Add members'),
936
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
937

    
938
    def __init__(self, *args, **kwargs):
939
        chain_id = kwargs.pop('chain_id', None)
940
        if chain_id:
941
            self.project = Project.objects.get(id=chain_id)
942
        self.request_user = kwargs.pop('request_user', None)
943
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
944

    
945
    def clean(self):
946
        try:
947
            accept_membership_checks(self.project, self.request_user)
948
        except PermissionDenied, e:
949
            raise forms.ValidationError(e)
950

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

    
962
    def get_valid_users(self):
963
        """Should be called after form cleaning"""
964
        try:
965
            return self.valid_users
966
        except:
967
            return ()
968

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

    
979

    
980
class ProjectSearchForm(forms.Form):
981
    q = forms.CharField(max_length=200, label='Search project', required=False)
982

    
983

    
984
class ExtendedProfileForm(ProfileForm):
985
    """
986
    Profile form that combines `email change` and `password change` user
987
    actions by propagating submited data to internal EmailChangeForm
988
    and ExtendedPasswordChangeForm objects.
989
    """
990

    
991
    password_change_form = None
992
    email_change_form = None
993

    
994
    password_change = False
995
    email_change = False
996

    
997
    extra_forms_fields = {
998
        'email': ['new_email_address'],
999
        'password': ['old_password', 'new_password1', 'new_password2']
1000
    }
1001

    
1002
    fields = ('email')
1003
    change_password = forms.BooleanField(initial=False, required=False)
1004
    change_email = forms.BooleanField(initial=False, required=False)
1005

    
1006
    email_changed = False
1007
    password_changed = False
1008

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

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

    
1037
        if EMAILCHANGE_ENABLED and self.instance.can_change_email():
1038
            self.email_change = True
1039
        else:
1040
            self.fields_list.remove('new_email_address')
1041
            self.fields_list.remove('change_email')
1042
            del self.fields['change_email']
1043

    
1044
        self._init_extra_forms()
1045
        self.save_extra_forms = []
1046
        self.success_messages = []
1047
        self.fields.keyOrder = self.fields_list
1048

    
1049

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

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

    
1068
    def _update_extra_form_errors(self):
1069
        if self.cleaned_data.get('change_password'):
1070
            self.errors.update(self.password_change_form.errors)
1071
        if self.cleaned_data.get('change_email'):
1072
            self.errors.update(self.email_change_form.errors)
1073

    
1074
    def _init_extra_forms(self):
1075
        self.email_change_form = EmailChangeForm(self.data)
1076
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1077
                                   data=self.data, session_key=self.session_key)
1078
        self._init_extra_form_fields()
1079

    
1080
    def is_valid(self):
1081
        password, email = True, True
1082
        profile = super(ExtendedProfileForm, self).is_valid()
1083
        if profile and self.cleaned_data.get('change_password', None):
1084

    
1085
            password = self.password_change_form.is_valid()
1086
            self.save_extra_forms.append('password')
1087
        if profile and self.cleaned_data.get('change_email'):
1088
            self.fields['new_email_address'].required = True
1089
            email = self.email_change_form.is_valid()
1090
            self.save_extra_forms.append('email')
1091

    
1092
        if not password or not email:
1093
            self._update_extra_form_errors()
1094

    
1095
        return all([profile, password, email])
1096

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