Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 2dc27ac1

History | View | Annotate | Download (41.3 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.date_signed_terms = datetime.now()
195
        user.renew_token()
196
        if commit:
197
            user.save(**kwargs)
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, **kwargs):
222
        user = super(InvitedLocalUserCreationForm, self).save(commit=False,
223
                                                              **kwargs)
224
        user.set_invitations_level()
225
        user.email_verified = True
226
        if commit:
227
            user.save(**kwargs)
228
        return user
229

    
230

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
312

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

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

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

    
339

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

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

    
353

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

    
358

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

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

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

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

    
384
    def clean_username(self):
385
        return self.cleaned_data['username'].lower()
386

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

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

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

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

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

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

    
435

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

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

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

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

    
462
    def clean_email(self):
463
        return self.instance.email
464

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

    
477

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

    
486

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

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

    
496

    
497
class ExtendedPasswordResetForm(PasswordResetForm):
498
    """
499
    Extends PasswordResetForm by overriding
500

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

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

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

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

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

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

    
556

    
557
class EmailChangeForm(forms.ModelForm):
558

    
559
    class Meta:
560
        model = EmailChange
561
        fields = ('new_email_address',)
562

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

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

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

    
584

    
585
class SignApprovalTermsForm(forms.ModelForm):
586

    
587
    class Meta:
588
        model = AstakosUser
589
        fields = ("has_signed_terms",)
590

    
591
    def __init__(self, *args, **kwargs):
592
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
593

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

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

    
607

    
608
class InvitationForm(forms.ModelForm):
609

    
610
    username = forms.EmailField(label=_("Email"))
611

    
612
    def __init__(self, *args, **kwargs):
613
        super(InvitationForm, self).__init__(*args, **kwargs)
614

    
615
    class Meta:
616
        model = Invitation
617
        fields = ('username', 'realname')
618

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

    
629

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

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

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

    
657

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

    
670
    def __init__(self, user, *args, **kwargs):
671
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
672

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

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

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

    
690

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

    
706

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

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

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

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

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

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

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

    
758
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
759
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
760

    
761

    
762
class ProjectApplicationForm(forms.ModelForm):
763

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

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

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

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

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

    
793
    end_date = forms.DateTimeField(
794
        label=app_end_date_label,
795
        help_text=app_end_date_help)
796

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

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

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

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

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

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

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

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

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

    
903
        ordered_keys = presentation.RESOURCES['resources_order']
904

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

    
911
        policies = sorted(policies, key=resource_order)
912
        return policies
913

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

    
922
        return policies
923

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

    
933

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

    
961

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

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

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

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

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

    
1003

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

    
1014

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

    
1018

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

    
1026
    password_change_form = None
1027
    email_change_form = None
1028

    
1029
    password_change = False
1030
    email_change = False
1031

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

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

    
1041
    email_changed = False
1042
    password_changed = False
1043

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

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

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

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

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

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

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

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

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

    
1126
        if not password or not email:
1127
            self._update_extra_form_errors()
1128

    
1129
        return all([profile, password, email])
1130

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