Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 36f1eabb

History | View | Annotate | Download (40.7 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
        self.request = kwargs.pop('request', None)
249
        if not self.provider or self.provider == 'local':
250
            raise Exception('Invalid provider, %r' % self.provider)
251

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

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

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

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

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

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

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

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

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

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

    
310

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

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

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

    
335

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

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

    
348

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

    
353

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

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

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

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

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

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

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

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

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

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

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

    
429

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

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

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

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

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

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

    
470

    
471

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

    
480

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

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

    
490

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

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

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

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

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

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

    
547

    
548
class EmailChangeForm(forms.ModelForm):
549

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

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

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

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

    
573

    
574
class SignApprovalTermsForm(forms.ModelForm):
575

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

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

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

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

    
596

    
597
class InvitationForm(forms.ModelForm):
598

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

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

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

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

    
617

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

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

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

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

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

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

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

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

    
674

    
675

    
676

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

    
692

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

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

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

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

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

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

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

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

    
747
class ProjectApplicationForm(forms.ModelForm):
748

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
907

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

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

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

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

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

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

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

    
981

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

    
985

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

    
993
    password_change_form = None
994
    email_change_form = None
995

    
996
    password_change = False
997
    email_change = False
998

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

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

    
1008
    email_changed = False
1009
    password_changed = False
1010

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

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

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

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

    
1048

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

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

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

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

    
1079
    def is_valid(self):
1080
        password, email = True, True
1081
        profile = super(ExtendedProfileForm, self).is_valid()
1082
        if profile and self.cleaned_data.get('change_password', None):
1083
            self.password_change_form.fields['new_password1'].required = True
1084
            self.password_change_form.fields['new_password2'].required = True
1085
            password = self.password_change_form.is_valid()
1086
            self.save_extra_forms.append('password')
1087
        if profile and self.cleaned_data.get('change_email'):
1088
            self.fields['new_email_address'].required = True
1089
            email = self.email_change_form.is_valid()
1090
            self.save_extra_forms.append('email')
1091

    
1092
        if not password or not email:
1093
            self._update_extra_form_errors()
1094

    
1095
        return all([profile, password, email])
1096

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