Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 8998f09a

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.db import transaction
46
from django.core import validators
47
from django.core.exceptions import PermissionDenied
48

    
49
from synnefo_branding.utils import render_to_string
50
from synnefo.lib import join_urls
51
from astakos.im.models import AstakosUser, EmailChange, Invitation, Resource, \
52
    PendingThirdPartyUser, get_latest_terms, ProjectApplication, Project
53
from astakos.im import presentation
54
from astakos.im.widgets import DummyWidget, RecaptchaWidget
55
from astakos.im.functions import send_change_email, submit_application, \
56
    accept_membership_checks
57

    
58
from astakos.im.util import reserved_verified_email, model_to_dict
59
from astakos.im import auth_providers
60
from astakos.im import settings
61

    
62
import astakos.im.messages as astakos_messages
63

    
64
import logging
65
import hashlib
66
import recaptcha.client.captcha as captcha
67
import re
68

    
69
logger = logging.getLogger(__name__)
70

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

    
75

    
76
class StoreUserMixin(object):
77

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

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

    
93

    
94
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
95
    """
96
    Extends the built in UserCreationForm in several ways:
97

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

    
107
    class Meta:
108
        model = AstakosUser
109
        fields = ("email", "first_name", "last_name",
110
                  "has_signed_terms", "has_signed_terms")
111

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

    
119
        # we only use LocalUserCreationForm for local provider
120
        if not provider == 'local':
121
            raise Exception('Invalid provider')
122

    
123
        if request:
124
            self.ip = request.META.get('REMOTE_ADDR',
125
                                       request.META.get('HTTP_X_REAL_IP',
126
                                                        None))
127

    
128
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
129
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
130
                                'password1', 'password2']
131

    
132
        if settings.RECAPTCHA_ENABLED:
133
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
134
                                         'recaptcha_response_field', ])
135
        if get_latest_terms():
136
            self.fields.keyOrder.append('has_signed_terms')
137

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

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

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

    
160
    def clean_recaptcha_response_field(self):
161
        if 'recaptcha_challenge_field' in self.cleaned_data:
162
            self.validate_captcha()
163
        return self.cleaned_data['recaptcha_response_field']
164

    
165
    def clean_recaptcha_challenge_field(self):
166
        if 'recaptcha_response_field' in self.cleaned_data:
167
            self.validate_captcha()
168
        return self.cleaned_data['recaptcha_challenge_field']
169

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

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

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

    
200

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

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

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

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

    
228

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
308

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

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

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

    
333

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

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

    
346

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

    
351

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

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

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

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

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

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

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

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

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

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

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

    
427

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

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

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

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

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

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

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

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

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

    
479

    
480

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

    
489

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

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

    
499

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

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

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

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

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

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

    
556

    
557
class EmailChangeForm(forms.ModelForm):
558

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

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

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

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

    
582

    
583
class SignApprovalTermsForm(forms.ModelForm):
584

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

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

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

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

    
605

    
606
class InvitationForm(forms.ModelForm):
607

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

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

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

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

    
626

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

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

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

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

    
664
    def __init__(self, user, *args, **kwargs):
665
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
666

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

    
675
            provider = auth_providers.get_provider('local', self.user)
676
            if provider.get_add_policy:
677
                provider.add_to_user()
678

    
679
        except BaseException, e:
680
            logger.exception(e)
681
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
682

    
683

    
684

    
685

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

    
701

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

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

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

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

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

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

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

    
753
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
754
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
755

    
756
class ProjectApplicationForm(forms.ModelForm):
757

    
758
    name = forms.CharField(
759
        label     = app_name_label,
760
        help_text = app_name_help,
761
        widget    = app_name_widget,
762
        validators = [app_name_validator])
763

    
764
    homepage = forms.URLField(
765
        label     = app_home_label,
766
        help_text = app_home_help,
767
        widget    = app_home_widget,
768
        required  = False)
769

    
770
    description = forms.CharField(
771
        label     = app_desc_label,
772
        help_text = app_desc_help,
773
        widget    = forms.Textarea,
774
        required  = False)
775

    
776
    comments = forms.CharField(
777
        label     = app_comment_label,
778
        help_text = app_comment_help,
779
        widget    = forms.Textarea,
780
        required  = False)
781

    
782
    start_date = forms.DateTimeField(
783
        label     = app_start_date_label,
784
        help_text = app_start_date_help,
785
        required  = False)
786

    
787
    end_date = forms.DateTimeField(
788
        label     = app_end_date_label,
789
        help_text = app_end_date_help)
790

    
791
    member_join_policy  = forms.TypedChoiceField(
792
        label     = join_policy_label,
793
        help_text = app_member_join_policy_help,
794
        initial   = 2,
795
        coerce    = int,
796
        choices   = join_policies)
797

    
798
    member_leave_policy = forms.TypedChoiceField(
799
        label     = leave_policy_label,
800
        help_text = app_member_leave_policy_help,
801
        coerce    = int,
802
        choices   = leave_policies)
803

    
804
    limit_on_members_number = forms.IntegerField(
805
        label     = max_members_label,
806
        help_text = max_members_help,
807
        min_value = 0,
808
        required  = False)
809

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

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

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

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

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

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

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

    
901
        policies = sorted(policies, key=resource_order)
902
        return policies
903

    
904
    def cleaned_resource_policies(self):
905
        return [(d['name'], d['uplimit']) for d in self.resource_policies]
906

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

    
916

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

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

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

    
952
    def clean(self):
953
        try:
954
            accept_membership_checks(self.project, self.request_user)
955
        except PermissionDenied, e:
956
            raise forms.ValidationError(e)
957

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

    
969
    def get_valid_users(self):
970
        """Should be called after form cleaning"""
971
        try:
972
            return self.valid_users
973
        except:
974
            return ()
975

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

    
986

    
987
class ProjectSearchForm(forms.Form):
988
    q = forms.CharField(max_length=200, label='Search project', required=False)
989

    
990

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

    
998
    password_change_form = None
999
    email_change_form = None
1000

    
1001
    password_change = False
1002
    email_change = False
1003

    
1004
    extra_forms_fields = {
1005
        'email': ['new_email_address'],
1006
        'password': ['old_password', 'new_password1', 'new_password2']
1007
    }
1008

    
1009
    fields = ('email')
1010
    change_password = forms.BooleanField(initial=False, required=False)
1011
    change_email = forms.BooleanField(initial=False, required=False)
1012

    
1013
    email_changed = False
1014
    password_changed = False
1015

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

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

    
1044
        if settings.EMAILCHANGE_ENABLED and self.instance.can_change_email():
1045
            self.email_change = True
1046
        else:
1047
            self.fields_list.remove('new_email_address')
1048
            self.fields_list.remove('change_email')
1049
            del self.fields['change_email']
1050

    
1051
        self._init_extra_forms()
1052
        self.save_extra_forms = []
1053
        self.success_messages = []
1054
        self.fields.keyOrder = self.fields_list
1055

    
1056

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

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

    
1075
    def _update_extra_form_errors(self):
1076
        if self.cleaned_data.get('change_password'):
1077
            self.errors.update(self.password_change_form.errors)
1078
        if self.cleaned_data.get('change_email'):
1079
            self.errors.update(self.email_change_form.errors)
1080

    
1081
    def _init_extra_forms(self):
1082
        self.email_change_form = EmailChangeForm(self.data)
1083
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1084
                                   data=self.data, session_key=self.session_key)
1085
        self._init_extra_form_fields()
1086

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

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

    
1099
        if not password or not email:
1100
            self._update_extra_form_errors()
1101

    
1102
        return all([profile, password, email])
1103

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