Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ f12bcb3d

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

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

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

    
61
import astakos.im.messages as astakos_messages
62

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

    
68
logger = logging.getLogger(__name__)
69

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

    
74

    
75
class StoreUserMixin(object):
76

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

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

    
92

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

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

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

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

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

    
122
        self.ip = None
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
        self.request = kwargs.pop('request', 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(
328
            InvitedThirdPartyUserCreationForm, self).save(commit=False)
329
        user.set_invitation_level()
330
        user.email_verified = True
331
        if commit:
332
            user.save()
333
        return user
334

    
335

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

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

    
349

    
350
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
351
                                        InvitedThirdPartyUserCreationForm):
352
    pass
353

    
354

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

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

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

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

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

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

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

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

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

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

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

    
431

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

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

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

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

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

    
461
    def save(self, commit=True):
462
        user = super(ProfileForm, self).save(commit=False)
463
        user.is_verified = True
464
        if self.cleaned_data.get('renew'):
465
            user.renew_token(
466
                flush_sessions=True,
467
                current_key=self.session_key
468
            )
469
        if commit:
470
            user.save()
471
        return user
472

    
473

    
474
class FeedbackForm(forms.Form):
475
    """
476
    Form for writing feedback.
477
    """
478
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
479
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
480
                                    required=False)
481

    
482

    
483
class SendInvitationForm(forms.Form):
484
    """
485
    Form for sending an invitations
486
    """
487

    
488
    email = forms.EmailField(required=True, label='Email address')
489
    first_name = forms.EmailField(label='First name')
490
    last_name = forms.EmailField(label='Last name')
491

    
492

    
493
class ExtendedPasswordResetForm(PasswordResetForm):
494
    """
495
    Extends PasswordResetForm by overriding
496

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

    
511
            provider = auth_providers.get_provider('local', user)
512
            if not user.has_usable_password():
513
                msg = provider.get_unusable_password_msg
514
                raise forms.ValidationError(mark_safe(msg))
515

    
516
            if not user.can_change_password():
517
                msg = provider.get_cannot_change_password_msg
518
                raise forms.ValidationError(mark_safe(msg))
519

    
520
        except AstakosUser.DoesNotExist:
521
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
522
        return email
523

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

    
551

    
552
class EmailChangeForm(forms.ModelForm):
553

    
554
    class Meta:
555
        model = EmailChange
556
        fields = ('new_email_address',)
557

    
558
    def clean_new_email_address(self):
559
        addr = self.cleaned_data['new_email_address']
560
        if reserved_verified_email(addr):
561
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
562
        return addr
563

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

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

    
579

    
580
class SignApprovalTermsForm(forms.ModelForm):
581

    
582
    class Meta:
583
        model = AstakosUser
584
        fields = ("has_signed_terms",)
585

    
586
    def __init__(self, *args, **kwargs):
587
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
588

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

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

    
602

    
603
class InvitationForm(forms.ModelForm):
604

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

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

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

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

    
624

    
625
class ExtendedPasswordChangeForm(PasswordChangeForm):
626
    """
627
    Extends PasswordChangeForm by enabling user
628
    to optionally renew also the token.
629
    """
630
    if not settings.NEWPASSWD_INVALIDATE_TOKEN:
631
        renew = forms.BooleanField(
632
            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 settings.NEWPASSWD_INVALIDATE_TOKEN or \
643
                    self.cleaned_data.get('renew'):
644
                self.user.renew_token()
645
            self.user.flush_sessions(current_key=self.session_key)
646
        except AttributeError:
647
            # if user model does has not such methods
648
            pass
649
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
650

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

    
699

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

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

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

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

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

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

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

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

    
754

    
755
class ProjectApplicationForm(forms.ModelForm):
756

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
892
        ordered_keys = presentation.RESOURCES['resources_order']
893

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

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

    
903
    def cleaned_resource_policies(self):
904
        policies = {}
905
        for d in self.resource_policies:
906
            policies[d["name"]] = {
907
                "project_capacity": None,
908
                "member_capacity": d["uplimit"]
909
            }
910

    
911
        return policies
912

    
913
    def save(self, commit=True):
914
        data = dict(self.cleaned_data)
915
        is_new = self.instance.id is None
916
        data['project_id'] = self.instance.chain.id if not is_new else None
917
        data['owner'] = self.user if is_new else self.instance.owner
918
        data['resources'] = self.cleaned_resource_policies()
919
        data['request_user'] = self.user
920
        submit_application(**data)
921

    
922

    
923
class ProjectSortForm(forms.Form):
924
    sorting = forms.ChoiceField(
925
        label='Sort by',
926
        choices=(('name', 'Sort by Name'),
927
                 ('issue_date', 'Sort by Issue date'),
928
                 ('start_date', 'Sort by Start Date'),
929
                 ('end_date', 'Sort by End Date'),
930
                 # ('approved_members_num', 'Sort by Participants'),
931
                 ('state', 'Sort by Status'),
932
                 ('member_join_policy__description',
933
                  'Sort by Member Join Policy'),
934
                 ('member_leave_policy__description',
935
                  'Sort by Member Leave Policy'),
936
                 ('-name', 'Sort by Name'),
937
                 ('-issue_date', 'Sort by Issue date'),
938
                 ('-start_date', 'Sort by Start Date'),
939
                 ('-end_date', 'Sort by End Date'),
940
                 # ('-approved_members_num', 'Sort by Participants'),
941
                 ('-state', 'Sort by Status'),
942
                 ('-member_join_policy__description',
943
                  'Sort by Member Join Policy'),
944
                 ('-member_leave_policy__description',
945
                  'Sort by Member Leave Policy')
946
                 ),
947
        required=True
948
    )
949

    
950

    
951
class AddProjectMembersForm(forms.Form):
952
    q = forms.CharField(
953
        widget=forms.Textarea(
954
            attrs={
955
                'placeholder':
956
                astakos_messages.ADD_PROJECT_MEMBERS_Q_PLACEHOLDER}),
957
        label=_('Add members'),
958
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
959
        required=True,)
960

    
961
    def __init__(self, *args, **kwargs):
962
        chain_id = kwargs.pop('chain_id', None)
963
        if chain_id:
964
            self.project = Project.objects.get(id=chain_id)
965
        self.request_user = kwargs.pop('request_user', None)
966
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
967

    
968
    def clean(self):
969
        try:
970
            accept_membership_project_checks(self.project, self.request_user)
971
        except ProjectError as e:
972
            raise forms.ValidationError(e)
973

    
974
        q = self.cleaned_data.get('q') or ''
975
        users = q.split(',')
976
        users = list(u.strip() for u in users if u)
977
        db_entries = AstakosUser.objects.verified().filter(email__in=users)
978
        unknown = list(set(users) - set(u.email for u in db_entries))
979
        if unknown:
980
            raise forms.ValidationError(
981
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
982
        self.valid_users = db_entries
983
        return self.cleaned_data
984

    
985
    def get_valid_users(self):
986
        """Should be called after form cleaning"""
987
        try:
988
            return self.valid_users
989
        except:
990
            return ()
991

    
992

    
993
class ProjectMembersSortForm(forms.Form):
994
    sorting = forms.ChoiceField(
995
        label='Sort by',
996
        choices=(('person__email', 'User Id'),
997
                 ('person__first_name', 'Name'),
998
                 ('acceptance_date', 'Acceptance date')
999
                 ),
1000
        required=True
1001
    )
1002

    
1003

    
1004
class ProjectSearchForm(forms.Form):
1005
    q = forms.CharField(max_length=200, label='Search project', required=False)
1006

    
1007

    
1008
class ExtendedProfileForm(ProfileForm):
1009
    """
1010
    Profile form that combines `email change` and `password change` user
1011
    actions by propagating submited data to internal EmailChangeForm
1012
    and ExtendedPasswordChangeForm objects.
1013
    """
1014

    
1015
    password_change_form = None
1016
    email_change_form = None
1017

    
1018
    password_change = False
1019
    email_change = False
1020

    
1021
    extra_forms_fields = {
1022
        'email': ['new_email_address'],
1023
        'password': ['old_password', 'new_password1', 'new_password2']
1024
    }
1025

    
1026
    fields = ('email')
1027
    change_password = forms.BooleanField(initial=False, required=False)
1028
    change_email = forms.BooleanField(initial=False, required=False)
1029

    
1030
    email_changed = False
1031
    password_changed = False
1032

    
1033
    def __init__(self, *args, **kwargs):
1034
        session_key = kwargs.get('session_key', None)
1035
        self.fields_list = [
1036
            'email',
1037
            'new_email_address',
1038
            'first_name',
1039
            'last_name',
1040
            'old_password',
1041
            'new_password1',
1042
            'new_password2',
1043
            'change_email',
1044
            'change_password',
1045
        ]
1046

    
1047
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
1048
        self.session_key = session_key
1049
        if self.instance.can_change_password():
1050
            self.password_change = True
1051
        else:
1052
            self.fields_list.remove('old_password')
1053
            self.fields_list.remove('new_password1')
1054
            self.fields_list.remove('new_password2')
1055
            self.fields_list.remove('change_password')
1056
            del self.fields['change_password']
1057

    
1058
        if settings.EMAILCHANGE_ENABLED and self.instance.can_change_email():
1059
            self.email_change = True
1060
        else:
1061
            self.fields_list.remove('new_email_address')
1062
            self.fields_list.remove('change_email')
1063
            del self.fields['change_email']
1064

    
1065
        self._init_extra_forms()
1066
        self.save_extra_forms = []
1067
        self.success_messages = []
1068
        self.fields.keyOrder = self.fields_list
1069

    
1070
    def _init_extra_form_fields(self):
1071
        if self.email_change:
1072
            self.fields.update(self.email_change_form.fields)
1073
            self.fields['new_email_address'].required = False
1074
            self.fields['email'].help_text = _(
1075
                'Change the email associated with '
1076
                'your account. This email will '
1077
                'remain active until you verify '
1078
                'your new one.')
1079

    
1080
        if self.password_change:
1081
            self.fields.update(self.password_change_form.fields)
1082
            self.fields['old_password'].required = False
1083
            self.fields['old_password'].label = _('Password')
1084
            self.fields['old_password'].help_text = _('Change your password.')
1085
            self.fields['old_password'].initial = 'password'
1086
            self.fields['new_password1'].required = False
1087
            self.fields['new_password2'].required = False
1088

    
1089
    def _update_extra_form_errors(self):
1090
        if self.cleaned_data.get('change_password'):
1091
            self.errors.update(self.password_change_form.errors)
1092
        if self.cleaned_data.get('change_email'):
1093
            self.errors.update(self.email_change_form.errors)
1094

    
1095
    def _init_extra_forms(self):
1096
        self.email_change_form = EmailChangeForm(self.data)
1097
        self.password_change_form = ExtendedPasswordChangeForm(
1098
            user=self.instance,
1099
            data=self.data, session_key=self.session_key)
1100
        self._init_extra_form_fields()
1101

    
1102
    def is_valid(self):
1103
        password, email = True, True
1104
        profile = super(ExtendedProfileForm, self).is_valid()
1105
        if profile and self.cleaned_data.get('change_password', None):
1106
            self.password_change_form.fields['new_password1'].required = True
1107
            self.password_change_form.fields['new_password2'].required = True
1108
            password = self.password_change_form.is_valid()
1109
            self.save_extra_forms.append('password')
1110
        if profile and self.cleaned_data.get('change_email'):
1111
            self.fields['new_email_address'].required = True
1112
            email = self.email_change_form.is_valid()
1113
            self.save_extra_forms.append('email')
1114

    
1115
        if not password or not email:
1116
            self._update_extra_form_errors()
1117

    
1118
        return all([profile, password, email])
1119

    
1120
    def save(self, request, *args, **kwargs):
1121
        if 'email' in self.save_extra_forms:
1122
            self.email_change_form.save(request, *args, **kwargs)
1123
            self.email_changed = True
1124
        if 'password' in self.save_extra_forms:
1125
            self.password_change_form.save(*args, **kwargs)
1126
            self.password_changed = True
1127
        return super(ExtendedProfileForm, self).save(*args, **kwargs)