Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 95ab222e

History | View | Annotate | Download (41.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, *args, **kwargs):
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
                                                       *args, **kwargs)
195
        user.date_signed_terms = datetime.now()
196
        user.renew_token()
197
        if commit:
198
            user.save(*args, **kwargs)
199
            logger.info('Created user %s', user.log_display)
200
        return user
201

    
202

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

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

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

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

    
231

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

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

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

    
249
        self.provider = kwargs.pop('provider', None)
250
        self.request = kwargs.pop('request', None)
251
        if not self.provider or self.provider == 'local':
252
            raise Exception('Invalid provider, %r' % self.provider)
253

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

    
261
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
262

    
263
        if not get_latest_terms():
264
            del self.fields['has_signed_terms']
265

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

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

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

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

    
293
    def _get_pending_user(self):
294
        return PendingThirdPartyUser.objects.get(token=self.third_party_token)
295

    
296
    def post_store_user(self, user, request=None):
297
        pending = self._get_pending_user()
298
        provider = pending.get_provider(user)
299
        provider.add_to_user()
300
        pending.delete()
301

    
302
    def save(self, commit=True, *args, **kwargs):
303
        user = super(ThirdPartyUserCreationForm, self).save(commit=False,
304
                                                            *args, **kwargs)
305
        user.set_unusable_password()
306
        user.renew_token()
307
        user.date_signed_terms = datetime.now()
308
        if commit:
309
            user.save(*args, **kwargs)
310
            logger.info('Created user %s' % user.log_display)
311
        return user
312

    
313

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

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

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

    
339

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

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

    
352

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

    
357

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

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

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

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

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

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

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

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

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

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

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

    
433

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

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

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

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

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

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

    
474

    
475

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

    
484

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

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

    
494

    
495
class ExtendedPasswordResetForm(PasswordResetForm):
496
    """
497
    Extends PasswordResetForm by overriding
498

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

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

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

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

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

    
553

    
554
class EmailChangeForm(forms.ModelForm):
555

    
556
    class Meta:
557
        model = EmailChange
558
        fields = ('new_email_address',)
559

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

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

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

    
581

    
582
class SignApprovalTermsForm(forms.ModelForm):
583

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

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

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

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

    
604

    
605
class InvitationForm(forms.ModelForm):
606

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

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

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

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

    
625

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

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

    
640
    def save(self, commit=True, *args, **kwargs):
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
                                                            *args, **kwargs)
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, *args, **kwargs):
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
                                                         *args, **kwargs)
683

    
684

    
685

    
686

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

    
702

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

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

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

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

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

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

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

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

    
757
class ProjectApplicationForm(forms.ModelForm):
758

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
917

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

    
941
class AddProjectMembersForm(forms.Form):
942
    q = forms.CharField(
943
        widget=forms.Textarea(attrs={
944
            'placeholder': astakos_messages.ADD_PROJECT_MEMBERS_Q_PLACEHOLDER}
945
            ),
946
        label=_('Add members'),
947
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
948
        required=True,)
949

    
950
    def __init__(self, *args, **kwargs):
951
        chain_id = kwargs.pop('chain_id', None)
952
        if chain_id:
953
            self.project = Project.objects.get(id=chain_id)
954
        self.request_user = kwargs.pop('request_user', None)
955
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
956

    
957
    def clean(self):
958
        try:
959
            accept_membership_checks(self.project, self.request_user)
960
        except PermissionDenied, e:
961
            raise forms.ValidationError(e)
962

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

    
974
    def get_valid_users(self):
975
        """Should be called after form cleaning"""
976
        try:
977
            return self.valid_users
978
        except:
979
            return ()
980

    
981
class ProjectMembersSortForm(forms.Form):
982
    sorting = forms.ChoiceField(
983
        label='Sort by',
984
        choices=(('person__email', 'User Id'),
985
                 ('person__first_name', 'Name'),
986
                 ('acceptance_date', 'Acceptance date')
987
        ),
988
        required=True
989
    )
990

    
991

    
992
class ProjectSearchForm(forms.Form):
993
    q = forms.CharField(max_length=200, label='Search project', required=False)
994

    
995

    
996
class ExtendedProfileForm(ProfileForm):
997
    """
998
    Profile form that combines `email change` and `password change` user
999
    actions by propagating submited data to internal EmailChangeForm
1000
    and ExtendedPasswordChangeForm objects.
1001
    """
1002

    
1003
    password_change_form = None
1004
    email_change_form = None
1005

    
1006
    password_change = False
1007
    email_change = False
1008

    
1009
    extra_forms_fields = {
1010
        'email': ['new_email_address'],
1011
        'password': ['old_password', 'new_password1', 'new_password2']
1012
    }
1013

    
1014
    fields = ('email')
1015
    change_password = forms.BooleanField(initial=False, required=False)
1016
    change_email = forms.BooleanField(initial=False, required=False)
1017

    
1018
    email_changed = False
1019
    password_changed = False
1020

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

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

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

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

    
1058

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

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

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

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

    
1089
    def is_valid(self):
1090
        password, email = True, True
1091
        profile = super(ExtendedProfileForm, self).is_valid()
1092
        if profile and self.cleaned_data.get('change_password', None):
1093
            self.password_change_form.fields['new_password1'].required = True
1094
            self.password_change_form.fields['new_password2'].required = True
1095
            password = self.password_change_form.is_valid()
1096
            self.save_extra_forms.append('password')
1097
        if profile and self.cleaned_data.get('change_email'):
1098
            self.fields['new_email_address'].required = True
1099
            email = self.email_change_form.is_valid()
1100
            self.save_extra_forms.append('email')
1101

    
1102
        if not password or not email:
1103
            self._update_extra_form_errors()
1104

    
1105
        return all([profile, password, email])
1106

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