Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (40.5 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33
from random import random
34
from datetime import datetime
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
        self.ip = None
124
        if request:
125
            self.ip = request.META.get('REMOTE_ADDR',
126
                                       request.META.get('HTTP_X_REAL_IP',
127
                                                        None))
128

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

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

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

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

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

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

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

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

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

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

    
201

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

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

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

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

    
229

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
309

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

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

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

    
334

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

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

    
347

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

    
352

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

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

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

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

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

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

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

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

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

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

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

    
428

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

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

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

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

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

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

    
469

    
470

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

    
479

    
480
class SendInvitationForm(forms.Form):
481
    """
482
    Form for sending an invitations
483
    """
484

    
485
    email = forms.EmailField(required=True, label='Email address')
486
    first_name = forms.EmailField(label='First name')
487
    last_name = forms.EmailField(label='Last name')
488

    
489

    
490
class ExtendedPasswordResetForm(PasswordResetForm):
491
    """
492
    Extends PasswordResetForm by overriding
493

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

    
508
            provider = auth_providers.get_provider('local', user)
509
            if not user.has_usable_password():
510
                msg = provider.get_unusable_password_msg
511
                raise forms.ValidationError(mark_safe(msg))
512

    
513
            if not user.can_change_password():
514
                msg = provider.get_cannot_change_password_msg
515
                raise forms.ValidationError(mark_safe(msg))
516

    
517
        except AstakosUser.DoesNotExist:
518
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
519
        return email
520

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

    
546

    
547
class EmailChangeForm(forms.ModelForm):
548

    
549
    class Meta:
550
        model = EmailChange
551
        fields = ('new_email_address',)
552

    
553
    def clean_new_email_address(self):
554
        addr = self.cleaned_data['new_email_address']
555
        if reserved_verified_email(addr):
556
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
557
        return addr
558

    
559
    def save(self, request, email_template_name='registration/email_change_email.txt', commit=True):
560
        ec = super(EmailChangeForm, self).save(commit=False)
561
        ec.user = request.user
562
        # delete pending email changes
563
        request.user.emailchanges.all().delete()
564

    
565
        activation_key = hashlib.sha1(
566
            str(random()) + smart_str(ec.new_email_address))
567
        ec.activation_key = activation_key.hexdigest()
568
        if commit:
569
            ec.save()
570
        send_change_email(ec, request, email_template_name=email_template_name)
571

    
572

    
573
class SignApprovalTermsForm(forms.ModelForm):
574

    
575
    class Meta:
576
        model = AstakosUser
577
        fields = ("has_signed_terms",)
578

    
579
    def __init__(self, *args, **kwargs):
580
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
581

    
582
    def clean_has_signed_terms(self):
583
        has_signed_terms = self.cleaned_data['has_signed_terms']
584
        if not has_signed_terms:
585
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
586
        return has_signed_terms
587

    
588
    def save(self, commit=True):
589
        user = super(SignApprovalTermsForm, self).save(commit)
590
        user.date_signed_terms = datetime.now()
591
        if commit:
592
            user.save()
593
        return user
594

    
595

    
596
class InvitationForm(forms.ModelForm):
597

    
598
    username = forms.EmailField(label=_("Email"))
599

    
600
    def __init__(self, *args, **kwargs):
601
        super(InvitationForm, self).__init__(*args, **kwargs)
602

    
603
    class Meta:
604
        model = Invitation
605
        fields = ('username', 'realname')
606

    
607
    def clean_username(self):
608
        username = self.cleaned_data['username']
609
        try:
610
            Invitation.objects.get(username=username)
611
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
612
        except Invitation.DoesNotExist:
613
            pass
614
        return username
615

    
616

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

    
627
    def __init__(self, user, *args, **kwargs):
628
        self.session_key = kwargs.pop('session_key', None)
629
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
630

    
631
    def save(self, commit=True):
632
        try:
633
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
634
                    self.cleaned_data.get('renew'):
635
                self.user.renew_token()
636
            self.user.flush_sessions(current_key=self.session_key)
637
        except AttributeError:
638
            # if user model does has not such methods
639
            pass
640
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
641

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

    
654
    def __init__(self, user, *args, **kwargs):
655
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
656

    
657
    @transaction.commit_on_success()
658
    def save(self, commit=True):
659
        try:
660
            self.user = AstakosUser.objects.get(id=self.user.id)
661
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
662
                    self.cleaned_data.get('renew'):
663
                self.user.renew_token()
664

    
665
            provider = auth_providers.get_provider('local', self.user)
666
            if provider.get_add_policy:
667
                provider.add_to_user()
668

    
669
        except BaseException, e:
670
            logger.exception(e)
671
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
672

    
673

    
674

    
675

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

    
691

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

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

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

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

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

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

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

    
743
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
744
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
745

    
746
class ProjectApplicationForm(forms.ModelForm):
747

    
748
    name = forms.CharField(
749
        label     = app_name_label,
750
        help_text = app_name_help,
751
        widget    = app_name_widget,
752
        validators = [app_name_validator])
753

    
754
    homepage = forms.URLField(
755
        label     = app_home_label,
756
        help_text = app_home_help,
757
        widget    = app_home_widget,
758
        required  = False)
759

    
760
    description = forms.CharField(
761
        label     = app_desc_label,
762
        help_text = app_desc_help,
763
        widget    = forms.Textarea,
764
        required  = False)
765

    
766
    comments = forms.CharField(
767
        label     = app_comment_label,
768
        help_text = app_comment_help,
769
        widget    = forms.Textarea,
770
        required  = False)
771

    
772
    start_date = forms.DateTimeField(
773
        label     = app_start_date_label,
774
        help_text = app_start_date_help,
775
        required  = False)
776

    
777
    end_date = forms.DateTimeField(
778
        label     = app_end_date_label,
779
        help_text = app_end_date_help)
780

    
781
    member_join_policy  = forms.TypedChoiceField(
782
        label     = join_policy_label,
783
        help_text = app_member_join_policy_help,
784
        initial   = 2,
785
        coerce    = int,
786
        choices   = join_policies)
787

    
788
    member_leave_policy = forms.TypedChoiceField(
789
        label     = leave_policy_label,
790
        help_text = app_member_leave_policy_help,
791
        coerce    = int,
792
        choices   = leave_policies)
793

    
794
    limit_on_members_number = forms.IntegerField(
795
        label     = max_members_label,
796
        help_text = max_members_help,
797
        min_value = 0,
798
        required  = False)
799

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

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

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

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

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

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

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

    
891
        policies = sorted(policies, key=resource_order)
892
        return policies
893

    
894
    def cleaned_resource_policies(self):
895
        return [(d['name'], d['uplimit']) for d in self.resource_policies]
896

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

    
906

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

    
930
class AddProjectMembersForm(forms.Form):
931
    q = forms.CharField(
932
        widget=forms.Textarea(attrs={
933
            'placeholder': astakos_messages.ADD_PROJECT_MEMBERS_Q_PLACEHOLDER}
934
            ),
935
        label=_('Add members'),
936
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
937
        required=True,)
938

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

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

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

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

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

    
980

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

    
984

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

    
992
    password_change_form = None
993
    email_change_form = None
994

    
995
    password_change = False
996
    email_change = False
997

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

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

    
1007
    email_changed = False
1008
    password_changed = False
1009

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

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

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

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

    
1047

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

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

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

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

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

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

    
1090
        if not password or not email:
1091
            self._update_extra_form_errors()
1092

    
1093
        return all([profile, password, email])
1094

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