Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (41.4 kB)

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

    
36
from django import forms
37
from django.utils.translation import ugettext as _
38
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \
39
    PasswordResetForm, PasswordChangeForm, SetPasswordForm
40
from django.core.mail import send_mail, get_connection
41
from django.contrib.auth.tokens import default_token_generator
42
from django.core.urlresolvers import reverse
43
from django.utils.safestring import mark_safe
44
from django.utils.encoding import smart_str
45
from django.db import transaction
46
from django.core import validators
47

    
48
from synnefo.util import units
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_project_checks, ProjectError
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, **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, **kwargs)
194
        user.has_signed_terms = True
195
        user.date_signed_terms = datetime.now()
196
        user.renew_token()
197
        if commit:
198
            user.save(**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, **kwargs):
223
        user = super(InvitedLocalUserCreationForm, self).save(commit=False,
224
                                                              **kwargs)
225
        user.set_invitations_level()
226
        user.email_verified = True
227
        if commit:
228
            user.save(**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, **kwargs):
303
        user = super(ThirdPartyUserCreationForm, self).save(commit=False,
304
                                                            **kwargs)
305
        user.set_unusable_password()
306
        user.renew_token()
307
        user.has_signed_terms = True
308
        user.date_signed_terms = datetime.now()
309
        if commit:
310
            user.save(**kwargs)
311
            logger.info('Created user %s' % user.log_display)
312
        return user
313

    
314

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

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

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

    
341

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

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

    
355

    
356
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
357
                                        InvitedThirdPartyUserCreationForm):
358
    pass
359

    
360

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

    
367
    def __init__(self, *args, **kwargs):
368
        was_limited = kwargs.get('was_limited', False)
369
        request = kwargs.get('request', None)
370
        if request:
371
            self.ip = request.META.get(
372
                'REMOTE_ADDR',
373
                request.META.get('HTTP_X_REAL_IP', None))
374

    
375
        t = ('request', 'was_limited')
376
        for elem in t:
377
            if elem in kwargs.keys():
378
                kwargs.pop(elem)
379
        super(LoginForm, self).__init__(*args, **kwargs)
380

    
381
        self.fields.keyOrder = ['username', 'password']
382
        if was_limited and settings.RECAPTCHA_ENABLED:
383
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
384
                                         'recaptcha_response_field', ])
385

    
386
    def clean_username(self):
387
        return self.cleaned_data['username'].lower()
388

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

    
394
    def clean_recaptcha_challenge_field(self):
395
        if 'recaptcha_response_field' in self.cleaned_data:
396
            self.validate_captcha()
397
        return self.cleaned_data['recaptcha_challenge_field']
398

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

    
408
    def clean(self):
409
        """
410
        Override default behavior in order to check user's activation later
411
        """
412
        username = self.cleaned_data.get('username')
413

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

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

    
437

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

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

    
451
    class Meta:
452
        model = AstakosUser
453
        fields = ('email', 'first_name', 'last_name')
454

    
455
    def __init__(self, *args, **kwargs):
456
        self.session_key = kwargs.pop('session_key', None)
457
        super(ProfileForm, self).__init__(*args, **kwargs)
458
        instance = getattr(self, 'instance', None)
459
        ro_fields = ('email',)
460
        if instance and instance.id:
461
            for field in ro_fields:
462
                self.fields[field].widget.attrs['readonly'] = True
463

    
464
    def clean_email(self):
465
        return self.instance.email
466

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

    
479

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

    
488

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

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

    
498

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

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

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

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

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

    
530
    def save(self, domain_override=None,
531
             email_template_name='registration/password_reset_email.html',
532
             use_https=False, token_generator=default_token_generator,
533
             request=None, **kwargs):
534
        """
535
        Generates a one-use only link for resetting password and sends to the
536
        user.
537

538
        """
539
        for user in self.users_cache:
540
            url = user.astakosuser.get_password_reset_url(token_generator)
541
            url = join_urls(settings.BASE_HOST, url)
542
            c = {
543
                'email': user.email,
544
                'url': url,
545
                'site_name': settings.SITENAME,
546
                'user': user,
547
                'baseurl': settings.BASE_URL,
548
                'support': settings.CONTACT_EMAIL
549
            }
550
            message = render_to_string(email_template_name, c)
551
            from_email = settings.SERVER_EMAIL
552
            send_mail(_(astakos_messages.PASSWORD_RESET_EMAIL_SUBJECT),
553
                      message,
554
                      from_email,
555
                      [user.email],
556
                      connection=get_connection())
557

    
558

    
559
class EmailChangeForm(forms.ModelForm):
560

    
561
    class Meta:
562
        model = EmailChange
563
        fields = ('new_email_address',)
564

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

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

    
579
        activation_key = hashlib.sha1(
580
            str(random()) + smart_str(ec.new_email_address))
581
        ec.activation_key = activation_key.hexdigest()
582
        if commit:
583
            ec.save(**kwargs)
584
        send_change_email(ec, request, email_template_name=email_template_name)
585

    
586

    
587
class SignApprovalTermsForm(forms.ModelForm):
588

    
589
    class Meta:
590
        model = AstakosUser
591
        fields = ("has_signed_terms",)
592

    
593
    def __init__(self, *args, **kwargs):
594
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
595

    
596
    def clean_has_signed_terms(self):
597
        has_signed_terms = self.cleaned_data['has_signed_terms']
598
        if not has_signed_terms:
599
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
600
        return has_signed_terms
601

    
602
    def save(self, commit=True, **kwargs):
603
        user = super(SignApprovalTermsForm, self).save(commit=commit, **kwargs)
604
        user.date_signed_terms = datetime.now()
605
        if commit:
606
            user.save(**kwargs)
607
        return user
608

    
609

    
610
class InvitationForm(forms.ModelForm):
611

    
612
    username = forms.EmailField(label=_("Email"))
613

    
614
    def __init__(self, *args, **kwargs):
615
        super(InvitationForm, self).__init__(*args, **kwargs)
616

    
617
    class Meta:
618
        model = Invitation
619
        fields = ('username', 'realname')
620

    
621
    def clean_username(self):
622
        username = self.cleaned_data['username']
623
        try:
624
            Invitation.objects.get(username=username)
625
            raise forms.ValidationError(
626
                _(astakos_messages.INVITATION_EMAIL_EXISTS))
627
        except Invitation.DoesNotExist:
628
            pass
629
        return username
630

    
631

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

    
643
    def __init__(self, user, *args, **kwargs):
644
        self.session_key = kwargs.pop('session_key', None)
645
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
646

    
647
    def save(self, commit=True, **kwargs):
648
        try:
649
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
650
                    self.cleaned_data.get('renew'):
651
                self.user.renew_token()
652
            self.user.flush_sessions(current_key=self.session_key)
653
        except AttributeError:
654
            # if user model does has not such methods
655
            pass
656
        return super(ExtendedPasswordChangeForm, self).save(commit=commit,
657
                                                            **kwargs)
658

    
659

    
660
class ExtendedSetPasswordForm(SetPasswordForm):
661
    """
662
    Extends SetPasswordForm by enabling user
663
    to optionally renew also the token.
664
    """
665
    if not settings.NEWPASSWD_INVALIDATE_TOKEN:
666
        renew = forms.BooleanField(
667
            label='Renew token',
668
            required=False,
669
            initial=True,
670
            help_text='Unsetting this may result in security risk.')
671

    
672
    def __init__(self, user, *args, **kwargs):
673
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
674

    
675
    @transaction.commit_on_success()
676
    def save(self, commit=True, **kwargs):
677
        try:
678
            self.user = AstakosUser.objects.get(id=self.user.id)
679
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
680
                    self.cleaned_data.get('renew'):
681
                self.user.renew_token()
682

    
683
            provider = auth_providers.get_provider('local', self.user)
684
            if provider.get_add_policy:
685
                provider.add_to_user()
686

    
687
        except BaseException, e:
688
            logger.exception(e)
689
        return super(ExtendedSetPasswordForm, self).save(commit=commit,
690
                                                         **kwargs)
691

    
692

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

    
708

    
709
app_home_label = "Homepage URL"
710
app_home_placeholder = 'myinstitution.org/myproject/'
711
app_home_help = _("""
712
        URL pointing at your project's site.
713
        e.g.: myinstitution.org/myproject/.
714
        Leave blank if there is no website.""")
715
app_home_widget = forms.TextInput(
716
    attrs={'placeholder': app_home_placeholder})
717

    
718
app_desc_label = _("Description")
719
app_desc_help = _("""
720
        Please provide a short but descriptive abstract of your
721
        project, so that anyone searching can quickly understand
722
        what this project is about.""")
723

    
724
app_comment_label = _("Comments for review (private)")
725
app_comment_help = _("""
726
        Write down any comments you may have for the reviewer
727
        of this application (e.g. background and rationale to
728
        support your request).
729
        The comments are strictly for the review process
730
        and will not be made public.""")
731

    
732
app_start_date_label = _("Start date")
733
app_start_date_help = _("""
734
        Provide a date when your need your project to be created,
735
        and members to be able to join and get resources.
736
        This date is only a hint to help prioritize reviews.""")
737

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

    
745
join_policy_label = _("Joining policy")
746
app_member_join_policy_help = _("""
747
        Select how new members are accepted into the project.""")
748
leave_policy_label = _("Leaving policy")
749
app_member_leave_policy_help = _("""
750
        Select how new members can leave the project.""")
751

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

    
760
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
761
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
762

    
763

    
764
class ProjectApplicationForm(forms.ModelForm):
765

    
766
    name = forms.CharField(
767
        label=app_name_label,
768
        help_text=app_name_help,
769
        widget=app_name_widget,
770
        validators=[app_name_validator])
771

    
772
    homepage = forms.URLField(
773
        label=app_home_label,
774
        help_text=app_home_help,
775
        widget=app_home_widget,
776
        required=False)
777

    
778
    description = forms.CharField(
779
        label=app_desc_label,
780
        help_text=app_desc_help,
781
        widget=forms.Textarea,
782
        required=False)
783

    
784
    comments = forms.CharField(
785
        label=app_comment_label,
786
        help_text=app_comment_help,
787
        widget=forms.Textarea,
788
        required=False)
789

    
790
    start_date = forms.DateTimeField(
791
        label=app_start_date_label,
792
        help_text=app_start_date_help,
793
        required=False)
794

    
795
    end_date = forms.DateTimeField(
796
        label=app_end_date_label,
797
        help_text=app_end_date_help)
798

    
799
    member_join_policy = forms.TypedChoiceField(
800
        label=join_policy_label,
801
        help_text=app_member_join_policy_help,
802
        initial=2,
803
        coerce=int,
804
        choices=join_policies)
805

    
806
    member_leave_policy = forms.TypedChoiceField(
807
        label=leave_policy_label,
808
        help_text=app_member_leave_policy_help,
809
        coerce=int,
810
        choices=leave_policies)
811

    
812
    limit_on_members_number = forms.IntegerField(
813
        label=max_members_label,
814
        help_text=max_members_help,
815
        min_value=0,
816
        required=True)
817

    
818
    class Meta:
819
        model = ProjectApplication
820
        fields = ('name', 'homepage', 'description',
821
                  'start_date', 'end_date', 'comments',
822
                  'member_join_policy', 'member_leave_policy',
823
                  'limit_on_members_number')
824

    
825
    def __init__(self, *args, **kwargs):
826
        instance = kwargs.get('instance')
827
        self.precursor_application = instance
828
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
829
        # in case of new application remove closed join policy
830
        if not instance:
831
            policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.copy()
832
            policies.pop(3)
833
            self.fields['member_join_policy'].choices = policies.iteritems()
834

    
835
    def clean_start_date(self):
836
        start_date = self.cleaned_data.get('start_date')
837
        if not self.precursor_application:
838
            today = datetime.now()
839
            today = datetime(today.year, today.month, today.day)
840
            if start_date and (start_date - today).days < 0:
841
                raise forms.ValidationError(
842
                    _(astakos_messages.INVALID_PROJECT_START_DATE))
843
        return start_date
844

    
845
    def clean_end_date(self):
846
        start_date = self.cleaned_data.get('start_date')
847
        end_date = self.cleaned_data.get('end_date')
848
        today = datetime.now()
849
        today = datetime(today.year, today.month, today.day)
850
        if end_date and (end_date - today).days < 0:
851
            raise forms.ValidationError(
852
                _(astakos_messages.INVALID_PROJECT_END_DATE))
853
        if start_date and (end_date - start_date).days <= 0:
854
            raise forms.ValidationError(
855
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
856
        return end_date
857

    
858
    def clean(self):
859
        userid = self.data.get('user', None)
860
        self.resource_policies
861
        self.user = None
862
        if userid:
863
            try:
864
                self.user = AstakosUser.objects.get(id=userid)
865
            except AstakosUser.DoesNotExist:
866
                pass
867
        if not self.user:
868
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
869
        super(ProjectApplicationForm, self).clean()
870
        return self.cleaned_data
871

    
872
    @property
873
    def resource_policies(self):
874
        policies = []
875
        append = policies.append
876
        for name, value in self.data.iteritems():
877
            if not value:
878
                continue
879
            uplimit = value
880
            if name.endswith('_uplimit'):
881
                subs = name.split('_uplimit')
882
                prefix, suffix = subs
883
                try:
884
                    resource = Resource.objects.get(name=prefix)
885
                except Resource.DoesNotExist:
886
                    raise forms.ValidationError("Resource %s does not exist" %
887
                                                resource.name)
888
                # keep only resource limits for selected resource groups
889
                if self.data.get('is_selected_%s' %
890
                                 resource.group, "0") == "1":
891
                    if not resource.ui_visible:
892
                        raise forms.ValidationError("Invalid resource %s" %
893
                                                    resource.name)
894
                    d = model_to_dict(resource)
895
                    try:
896
                        uplimit = long(uplimit)
897
                    except ValueError:
898
                        m = "Limit should be an integer"
899
                        raise forms.ValidationError(m)
900
                    display = units.show(uplimit, resource.unit)
901
                    d.update(dict(resource=prefix, uplimit=uplimit,
902
                                  display_uplimit=display))
903
                    append(d)
904

    
905
        ordered_keys = presentation.RESOURCES['resources_order']
906

    
907
        def resource_order(r):
908
            if r['str_repr'] in ordered_keys:
909
                return ordered_keys.index(r['str_repr'])
910
            else:
911
                return -1
912

    
913
        policies = sorted(policies, key=resource_order)
914
        return policies
915

    
916
    def cleaned_resource_policies(self):
917
        policies = {}
918
        for d in self.resource_policies:
919
            policies[d["name"]] = {
920
                "project_capacity": None,
921
                "member_capacity": d["uplimit"]
922
            }
923

    
924
        return policies
925

    
926
    def save(self, commit=True, **kwargs):
927
        data = dict(self.cleaned_data)
928
        is_new = self.instance.id is None
929
        data['project_id'] = self.instance.chain.id if not is_new else None
930
        data['owner'] = self.user if is_new else self.instance.owner
931
        data['resources'] = self.cleaned_resource_policies()
932
        data['request_user'] = self.user
933
        submit_application(**data)
934

    
935

    
936
class ProjectSortForm(forms.Form):
937
    sorting = forms.ChoiceField(
938
        label='Sort by',
939
        choices=(('name', 'Sort by Name'),
940
                 ('issue_date', 'Sort by Issue date'),
941
                 ('start_date', 'Sort by Start Date'),
942
                 ('end_date', 'Sort by End Date'),
943
                 # ('approved_members_num', 'Sort by Participants'),
944
                 ('state', 'Sort by Status'),
945
                 ('member_join_policy__description',
946
                  'Sort by Member Join Policy'),
947
                 ('member_leave_policy__description',
948
                  'Sort by Member Leave Policy'),
949
                 ('-name', 'Sort by Name'),
950
                 ('-issue_date', 'Sort by Issue date'),
951
                 ('-start_date', 'Sort by Start Date'),
952
                 ('-end_date', 'Sort by End Date'),
953
                 # ('-approved_members_num', 'Sort by Participants'),
954
                 ('-state', 'Sort by Status'),
955
                 ('-member_join_policy__description',
956
                  'Sort by Member Join Policy'),
957
                 ('-member_leave_policy__description',
958
                  'Sort by Member Leave Policy')
959
                 ),
960
        required=True
961
    )
962

    
963

    
964
class AddProjectMembersForm(forms.Form):
965
    q = forms.CharField(
966
        widget=forms.Textarea(
967
            attrs={
968
                'placeholder':
969
                astakos_messages.ADD_PROJECT_MEMBERS_Q_PLACEHOLDER}),
970
        label=_('Add members'),
971
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
972
        required=True,)
973

    
974
    def __init__(self, *args, **kwargs):
975
        chain_id = kwargs.pop('chain_id', None)
976
        if chain_id:
977
            self.project = Project.objects.get(id=chain_id)
978
        self.request_user = kwargs.pop('request_user', None)
979
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
980

    
981
    def clean(self):
982
        try:
983
            accept_membership_project_checks(self.project, self.request_user)
984
        except ProjectError as e:
985
            raise forms.ValidationError(e)
986

    
987
        q = self.cleaned_data.get('q') or ''
988
        users = q.split(',')
989
        users = list(u.strip() for u in users if u)
990
        db_entries = AstakosUser.objects.verified().filter(email__in=users)
991
        unknown = list(set(users) - set(u.email for u in db_entries))
992
        if unknown:
993
            raise forms.ValidationError(
994
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
995
        self.valid_users = db_entries
996
        return self.cleaned_data
997

    
998
    def get_valid_users(self):
999
        """Should be called after form cleaning"""
1000
        try:
1001
            return self.valid_users
1002
        except:
1003
            return ()
1004

    
1005

    
1006
class ProjectMembersSortForm(forms.Form):
1007
    sorting = forms.ChoiceField(
1008
        label='Sort by',
1009
        choices=(('person__email', 'User Id'),
1010
                 ('person__first_name', 'Name'),
1011
                 ('acceptance_date', 'Acceptance date')
1012
                 ),
1013
        required=True
1014
    )
1015

    
1016

    
1017
class ProjectSearchForm(forms.Form):
1018
    q = forms.CharField(max_length=200, label='Search project', required=False)
1019

    
1020

    
1021
class ExtendedProfileForm(ProfileForm):
1022
    """
1023
    Profile form that combines `email change` and `password change` user
1024
    actions by propagating submited data to internal EmailChangeForm
1025
    and ExtendedPasswordChangeForm objects.
1026
    """
1027

    
1028
    password_change_form = None
1029
    email_change_form = None
1030

    
1031
    password_change = False
1032
    email_change = False
1033

    
1034
    extra_forms_fields = {
1035
        'email': ['new_email_address'],
1036
        'password': ['old_password', 'new_password1', 'new_password2']
1037
    }
1038

    
1039
    fields = ('email')
1040
    change_password = forms.BooleanField(initial=False, required=False)
1041
    change_email = forms.BooleanField(initial=False, required=False)
1042

    
1043
    email_changed = False
1044
    password_changed = False
1045

    
1046
    def __init__(self, *args, **kwargs):
1047
        session_key = kwargs.get('session_key', None)
1048
        self.fields_list = [
1049
            'email',
1050
            'new_email_address',
1051
            'first_name',
1052
            'last_name',
1053
            'old_password',
1054
            'new_password1',
1055
            'new_password2',
1056
            'change_email',
1057
            'change_password',
1058
        ]
1059

    
1060
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
1061
        self.session_key = session_key
1062
        if self.instance.can_change_password():
1063
            self.password_change = True
1064
        else:
1065
            self.fields_list.remove('old_password')
1066
            self.fields_list.remove('new_password1')
1067
            self.fields_list.remove('new_password2')
1068
            self.fields_list.remove('change_password')
1069
            del self.fields['change_password']
1070

    
1071
        if settings.EMAILCHANGE_ENABLED and self.instance.can_change_email():
1072
            self.email_change = True
1073
        else:
1074
            self.fields_list.remove('new_email_address')
1075
            self.fields_list.remove('change_email')
1076
            del self.fields['change_email']
1077

    
1078
        self._init_extra_forms()
1079
        self.save_extra_forms = []
1080
        self.success_messages = []
1081
        self.fields.keyOrder = self.fields_list
1082

    
1083
    def _init_extra_form_fields(self):
1084
        if self.email_change:
1085
            self.fields.update(self.email_change_form.fields)
1086
            self.fields['new_email_address'].required = False
1087
            self.fields['email'].help_text = _(
1088
                'Change the email associated with '
1089
                'your account. This email will '
1090
                'remain active until you verify '
1091
                'your new one.')
1092

    
1093
        if self.password_change:
1094
            self.fields.update(self.password_change_form.fields)
1095
            self.fields['old_password'].required = False
1096
            self.fields['old_password'].label = _('Password')
1097
            self.fields['old_password'].help_text = _('Change your password.')
1098
            self.fields['old_password'].initial = 'password'
1099
            self.fields['new_password1'].required = False
1100
            self.fields['new_password2'].required = False
1101

    
1102
    def _update_extra_form_errors(self):
1103
        if self.cleaned_data.get('change_password'):
1104
            self.errors.update(self.password_change_form.errors)
1105
        if self.cleaned_data.get('change_email'):
1106
            self.errors.update(self.email_change_form.errors)
1107

    
1108
    def _init_extra_forms(self):
1109
        self.email_change_form = EmailChangeForm(self.data)
1110
        self.password_change_form = ExtendedPasswordChangeForm(
1111
            user=self.instance,
1112
            data=self.data, session_key=self.session_key)
1113
        self._init_extra_form_fields()
1114

    
1115
    def is_valid(self):
1116
        password, email = True, True
1117
        profile = super(ExtendedProfileForm, self).is_valid()
1118
        if profile and self.cleaned_data.get('change_password', None):
1119
            self.password_change_form.fields['new_password1'].required = True
1120
            self.password_change_form.fields['new_password2'].required = True
1121
            password = self.password_change_form.is_valid()
1122
            self.save_extra_forms.append('password')
1123
        if profile and self.cleaned_data.get('change_email'):
1124
            self.fields['new_email_address'].required = True
1125
            email = self.email_change_form.is_valid()
1126
            self.save_extra_forms.append('email')
1127

    
1128
        if not password or not email:
1129
            self._update_extra_form_errors()
1130

    
1131
        return all([profile, password, email])
1132

    
1133
    def save(self, request, *args, **kwargs):
1134
        if 'email' in self.save_extra_forms:
1135
            self.email_change_form.save(request, *args, **kwargs)
1136
            self.email_changed = True
1137
        if 'password' in self.save_extra_forms:
1138
            self.password_change_form.save(*args, **kwargs)
1139
            self.password_changed = True
1140
        return super(ExtendedProfileForm, self).save(*args, **kwargs)