Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 67cf14bf

History | View | Annotate | Download (40.8 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
35

    
36
from django import forms
37
from django.utils.translation import ugettext as _
38
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \
39
    PasswordResetForm, PasswordChangeForm, SetPasswordForm
40
from django.core.mail import send_mail, get_connection
41
from django.contrib.auth.tokens import default_token_generator
42
from django.core.urlresolvers import reverse
43
from django.utils.safestring import mark_safe
44
from django.utils.encoding import smart_str
45
from django.conf import settings
46
from django.db import transaction
47
from django.core import validators
48
from django.core.exceptions import PermissionDenied
49

    
50
from synnefo_branding.utils import render_to_string
51
from synnefo.lib import join_urls
52
from astakos.im.models import AstakosUser, EmailChange, Invitation, Resource, \
53
    PendingThirdPartyUser, get_latest_terms, ProjectApplication, Project
54
from astakos.im.settings import BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, \
55
    RECAPTCHA_ENABLED, CONTACT_EMAIL, PASSWORD_RESET_EMAIL_SUBJECT, \
56
    NEWPASSWD_INVALIDATE_TOKEN, EMAILCHANGE_ENABLED
57
from astakos.im import presentation
58
from astakos.im.widgets import DummyWidget, RecaptchaWidget
59
from astakos.im.functions import send_change_email, submit_application, \
60
    accept_membership_checks
61

    
62
from astakos.im.util import reserved_verified_email, model_to_dict
63
from astakos.im import auth_providers
64

    
65
import astakos.im.messages as astakos_messages
66

    
67
import logging
68
import hashlib
69
import recaptcha.client.captcha as captcha
70
import re
71

    
72
logger = logging.getLogger(__name__)
73

    
74
DOMAIN_VALUE_REGEX = re.compile(
75
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
76
    re.IGNORECASE)
77

    
78

    
79
class StoreUserMixin(object):
80

    
81
    def store_user(self, user, request=None):
82
        """
83
        WARNING: this should be wrapped inside a transactional view/method.
84
        """
85
        user.save()
86
        self.post_store_user(user, request)
87
        return user
88

    
89
    def post_store_user(self, user, request):
90
        """
91
        Interface method for descendant backends to be able to do stuff within
92
        the transaction enabled by store_user.
93
        """
94
        pass
95

    
96

    
97
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
98
    """
99
    Extends the built in UserCreationForm in several ways:
100

101
    * Adds email, first_name, last_name, recaptcha_challenge_field,
102
    * recaptcha_response_field field.
103
    * The username field isn't visible and it is assigned a generated id.
104
    * User created is not active.
105
    """
106
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
107
    recaptcha_response_field = forms.CharField(
108
        widget=RecaptchaWidget, label='')
109

    
110
    class Meta:
111
        model = AstakosUser
112
        fields = ("email", "first_name", "last_name",
113
                  "has_signed_terms", "has_signed_terms")
114

    
115
    def __init__(self, *args, **kwargs):
116
        """
117
        Changes the order of fields, and removes the username field.
118
        """
119
        request = kwargs.pop('request', None)
120
        provider = kwargs.pop('provider', 'local')
121

    
122
        # we only use LocalUserCreationForm for local provider
123
        if not provider == 'local':
124
            raise Exception('Invalid provider')
125

    
126
        if request:
127
            self.ip = request.META.get('REMOTE_ADDR',
128
                                       request.META.get('HTTP_X_REAL_IP',
129
                                                        None))
130

    
131
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
132
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
133
                                'password1', 'password2']
134

    
135
        if RECAPTCHA_ENABLED:
136
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
137
                                         'recaptcha_response_field', ])
138
        if get_latest_terms():
139
            self.fields.keyOrder.append('has_signed_terms')
140

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

    
149
    def clean_email(self):
150
        email = self.cleaned_data['email']
151
        if not email:
152
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
153
        if reserved_verified_email(email):
154
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
155
        return email
156

    
157
    def clean_has_signed_terms(self):
158
        has_signed_terms = self.cleaned_data['has_signed_terms']
159
        if not has_signed_terms:
160
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
161
        return has_signed_terms
162

    
163
    def clean_recaptcha_response_field(self):
164
        if 'recaptcha_challenge_field' in self.cleaned_data:
165
            self.validate_captcha()
166
        return self.cleaned_data['recaptcha_response_field']
167

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

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

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

    
188
    def save(self, commit=True):
189
        """
190
        Saves the email, first_name and last_name properties, after the normal
191
        save behavior is complete.
192
        """
193
        user = super(LocalUserCreationForm, self).save(commit=False)
194
        user.date_signed_terms = datetime.now()
195
        user.renew_token()
196
        if commit:
197
            user.save()
198
            logger.info('Created user %s', user.log_display)
199
        return user
200

    
201

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

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

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

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

    
229

    
230
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
231
    email = forms.EmailField(
232
        label='Contact email',
233
        help_text='This is needed for contact purposes. '
234
        'It doesn&#39;t need to be the same with the one you '
235
        'provided to login previously. '
236
    )
237

    
238
    class Meta:
239
        model = AstakosUser
240
        fields = ['email', 'first_name', 'last_name', 'has_signed_terms']
241

    
242
    def __init__(self, *args, **kwargs):
243
        """
244
        Changes the order of fields, and removes the username field.
245
        """
246

    
247
        self.provider = kwargs.pop('provider', None)
248
        if not self.provider or self.provider == 'local':
249
            raise Exception('Invalid provider, %r' % self.provider)
250

    
251
        # ThirdPartyUserCreationForm should always get instantiated with
252
        # a third_party_token value
253
        self.third_party_token = kwargs.pop('third_party_token', None)
254
        if not self.third_party_token:
255
            raise Exception('ThirdPartyUserCreationForm'
256
                            ' requires third_party_token')
257

    
258
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
259

    
260
        if not get_latest_terms():
261
            del self.fields['has_signed_terms']
262

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

    
271
    def clean_email(self):
272
        email = self.cleaned_data['email']
273
        if not email:
274
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
275
        if reserved_verified_email(email):
276
            provider_id = self.provider
277
            provider = auth_providers.get_provider(provider_id)
278
            extra_message = provider.get_add_to_existing_account_msg
279

    
280
            raise forms.ValidationError(mark_safe(
281
                _(astakos_messages.EMAIL_USED) + ' ' + extra_message))
282
        return email
283

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

    
290
    def _get_pending_user(self):
291
        return PendingThirdPartyUser.objects.get(token=self.third_party_token)
292

    
293
    def post_store_user(self, user, request=None):
294
        pending = self._get_pending_user()
295
        provider = pending.get_provider(user)
296
        provider.add_to_user()
297
        pending.delete()
298

    
299
    def save(self, commit=True):
300
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
301
        user.set_unusable_password()
302
        user.renew_token()
303
        user.date_signed_terms = datetime.now()
304
        if commit:
305
            user.save()
306
            logger.info('Created user %s' % user.log_display)
307
        return user
308

    
309

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

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

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

    
334

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

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

    
347

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

    
352

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

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

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

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

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

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

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

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

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

    
403
        if username:
404
            try:
405
                user = AstakosUser.objects.get_by_identifier(username)
406
                if not user.has_auth_provider('local'):
407
                    provider = auth_providers.get_provider('local', user)
408
                    msg = provider.get_login_disabled_msg
409
                    raise forms.ValidationError(mark_safe(msg))
410
            except AstakosUser.DoesNotExist:
411
                pass
412

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

    
426

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

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

    
440
    class Meta:
441
        model = AstakosUser
442
        fields = ('email', 'first_name', 'last_name', 'auth_token',
443
                  'auth_token_expires', 'uuid')
444

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

    
454
    def clean_email(self):
455
        return self.instance.email
456

    
457
    def clean_auth_token(self):
458
        return self.instance.auth_token
459

    
460
    def clean_auth_token_expires(self):
461
        return self.instance.auth_token_expires
462

    
463
    def clean_uuid(self):
464
        return self.instance.uuid
465

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

    
478

    
479

    
480
class FeedbackForm(forms.Form):
481
    """
482
    Form for writing feedback.
483
    """
484
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
485
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
486
                                    required=False)
487

    
488

    
489
class SendInvitationForm(forms.Form):
490
    """
491
    Form for sending an invitations
492
    """
493

    
494
    email = forms.EmailField(required=True, label='Email address')
495
    first_name = forms.EmailField(label='First name')
496
    last_name = forms.EmailField(label='Last name')
497

    
498

    
499
class ExtendedPasswordResetForm(PasswordResetForm):
500
    """
501
    Extends PasswordResetForm by overriding
502

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

    
517
            provider = auth_providers.get_provider('local', user)
518
            if not user.has_usable_password():
519
                msg = provider.get_unusable_password_msg
520
                raise forms.ValidationError(mark_safe(msg))
521

    
522
            if not user.can_change_password():
523
                msg = provider.get_cannot_change_password_msg
524
                raise forms.ValidationError(mark_safe(msg))
525

    
526
        except AstakosUser.DoesNotExist:
527
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
528
        return email
529

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

    
555

    
556
class EmailChangeForm(forms.ModelForm):
557

    
558
    class Meta:
559
        model = EmailChange
560
        fields = ('new_email_address',)
561

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

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

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

    
581

    
582
class SignApprovalTermsForm(forms.ModelForm):
583

    
584
    class Meta:
585
        model = AstakosUser
586
        fields = ("has_signed_terms",)
587

    
588
    def __init__(self, *args, **kwargs):
589
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
590

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

    
597
    def save(self, commit=True):
598
        user = super(SignApprovalTermsForm, self).save(commit)
599
        user.date_signed_terms = datetime.now()
600
        if commit:
601
            user.save()
602
        return user
603

    
604

    
605
class InvitationForm(forms.ModelForm):
606

    
607
    username = forms.EmailField(label=_("Email"))
608

    
609
    def __init__(self, *args, **kwargs):
610
        super(InvitationForm, self).__init__(*args, **kwargs)
611

    
612
    class Meta:
613
        model = Invitation
614
        fields = ('username', 'realname')
615

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

    
625

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

    
636
    def __init__(self, user, *args, **kwargs):
637
        self.session_key = kwargs.pop('session_key', None)
638
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
639

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

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

    
662
    def __init__(self, user, *args, **kwargs):
663
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
664

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

    
672
            provider = auth_providers.get_provider('local', self.user)
673
            if provider.get_add_policy:
674
                provider.add_to_user()
675

    
676
        except BaseException, e:
677
            logger.exception(e)
678
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
679

    
680

    
681

    
682

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

    
698

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

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

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

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

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

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

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

    
750
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
751
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
752

    
753
class ProjectApplicationForm(forms.ModelForm):
754

    
755
    name = forms.CharField(
756
        label     = app_name_label,
757
        help_text = app_name_help,
758
        widget    = app_name_widget,
759
        validators = [app_name_validator])
760

    
761
    homepage = forms.URLField(
762
        label     = app_home_label,
763
        help_text = app_home_help,
764
        widget    = app_home_widget,
765
        required  = False)
766

    
767
    description = forms.CharField(
768
        label     = app_desc_label,
769
        help_text = app_desc_help,
770
        widget    = forms.Textarea,
771
        required  = False)
772

    
773
    comments = forms.CharField(
774
        label     = app_comment_label,
775
        help_text = app_comment_help,
776
        widget    = forms.Textarea,
777
        required  = False)
778

    
779
    start_date = forms.DateTimeField(
780
        label     = app_start_date_label,
781
        help_text = app_start_date_help,
782
        required  = False)
783

    
784
    end_date = forms.DateTimeField(
785
        label     = app_end_date_label,
786
        help_text = app_end_date_help)
787

    
788
    member_join_policy  = forms.TypedChoiceField(
789
        label     = join_policy_label,
790
        help_text = app_member_join_policy_help,
791
        initial   = 2,
792
        coerce    = int,
793
        choices   = join_policies)
794

    
795
    member_leave_policy = forms.TypedChoiceField(
796
        label     = leave_policy_label,
797
        help_text = app_member_leave_policy_help,
798
        coerce    = int,
799
        choices   = leave_policies)
800

    
801
    limit_on_members_number = forms.IntegerField(
802
        label     = max_members_label,
803
        help_text = max_members_help,
804
        min_value = 0,
805
        required  = False)
806

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

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

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

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

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

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

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

    
898
        policies = sorted(policies, key=resource_order)
899
        return policies
900

    
901
    def cleaned_resource_policies(self):
902
        return [(d['name'], d['uplimit']) for d in self.resource_policies]
903

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

    
913

    
914
class ProjectSortForm(forms.Form):
915
    sorting = forms.ChoiceField(
916
        label='Sort by',
917
        choices=(('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
                 ('-name', 'Sort by Name'),
926
                 ('-issue_date', 'Sort by Issue date'),
927
                 ('-start_date', 'Sort by Start Date'),
928
                 ('-end_date', 'Sort by End Date'),
929
#                  ('-approved_members_num', 'Sort by Participants'),
930
                 ('-state', 'Sort by Status'),
931
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
932
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
933
        ),
934
        required=True
935
    )
936

    
937
class AddProjectMembersForm(forms.Form):
938
    q = forms.CharField(
939
        max_length=800, widget=forms.Textarea, label=_('Add members'),
940
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
941

    
942
    def __init__(self, *args, **kwargs):
943
        chain_id = kwargs.pop('chain_id', None)
944
        if chain_id:
945
            self.project = Project.objects.get(id=chain_id)
946
        self.request_user = kwargs.pop('request_user', None)
947
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
948

    
949
    def clean(self):
950
        try:
951
            accept_membership_checks(self.project, self.request_user)
952
        except PermissionDenied, e:
953
            raise forms.ValidationError(e)
954

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

    
966
    def get_valid_users(self):
967
        """Should be called after form cleaning"""
968
        try:
969
            return self.valid_users
970
        except:
971
            return ()
972

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

    
983

    
984
class ProjectSearchForm(forms.Form):
985
    q = forms.CharField(max_length=200, label='Search project', required=False)
986

    
987

    
988
class ExtendedProfileForm(ProfileForm):
989
    """
990
    Profile form that combines `email change` and `password change` user
991
    actions by propagating submited data to internal EmailChangeForm
992
    and ExtendedPasswordChangeForm objects.
993
    """
994

    
995
    password_change_form = None
996
    email_change_form = None
997

    
998
    password_change = False
999
    email_change = False
1000

    
1001
    extra_forms_fields = {
1002
        'email': ['new_email_address'],
1003
        'password': ['old_password', 'new_password1', 'new_password2']
1004
    }
1005

    
1006
    fields = ('email')
1007
    change_password = forms.BooleanField(initial=False, required=False)
1008
    change_email = forms.BooleanField(initial=False, required=False)
1009

    
1010
    email_changed = False
1011
    password_changed = False
1012

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

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

    
1041
        if EMAILCHANGE_ENABLED and self.instance.can_change_email():
1042
            self.email_change = True
1043
        else:
1044
            self.fields_list.remove('new_email_address')
1045
            self.fields_list.remove('change_email')
1046
            del self.fields['change_email']
1047

    
1048
        self._init_extra_forms()
1049
        self.save_extra_forms = []
1050
        self.success_messages = []
1051
        self.fields.keyOrder = self.fields_list
1052

    
1053

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

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

    
1072
    def _update_extra_form_errors(self):
1073
        if self.cleaned_data.get('change_password'):
1074
            self.errors.update(self.password_change_form.errors)
1075
        if self.cleaned_data.get('change_email'):
1076
            self.errors.update(self.email_change_form.errors)
1077

    
1078
    def _init_extra_forms(self):
1079
        self.email_change_form = EmailChangeForm(self.data)
1080
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1081
                                   data=self.data, session_key=self.session_key)
1082
        self._init_extra_form_fields()
1083

    
1084
    def is_valid(self):
1085
        password, email = True, True
1086
        profile = super(ExtendedProfileForm, self).is_valid()
1087
        if profile and self.cleaned_data.get('change_password', None):
1088

    
1089
            password = self.password_change_form.is_valid()
1090
            self.save_extra_forms.append('password')
1091
        if profile and self.cleaned_data.get('change_email'):
1092
            self.fields['new_email_address'].required = True
1093
            email = self.email_change_form.is_valid()
1094
            self.save_extra_forms.append('email')
1095

    
1096
        if not password or not email:
1097
            self._update_extra_form_errors()
1098

    
1099
        return all([profile, password, email])
1100

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