Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (39.2 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 urlparse import urljoin
34
from random import random
35
from datetime import datetime, timedelta
36

    
37
from django import forms
38
from django.utils.translation import ugettext as _
39
from django.contrib.auth.forms import (
40
    UserCreationForm, AuthenticationForm,
41
    PasswordResetForm, PasswordChangeForm,
42
    SetPasswordForm)
43
from django.core.mail import send_mail, get_connection
44
from django.contrib.auth.tokens import default_token_generator
45
from django.template import Context, loader
46
from django.utils.http import int_to_base36
47
from django.core.urlresolvers import reverse
48
from django.utils.safestring import mark_safe
49
from django.utils.encoding import smart_str
50
from django.conf import settings
51
from django.forms.models import fields_for_model
52
from django.db import transaction
53
from django.utils.encoding import smart_unicode
54
from django.core import validators
55
from django.contrib.auth.models import AnonymousUser
56
from django.core.exceptions import PermissionDenied
57

    
58
from astakos.im.models import (
59
    AstakosUser, EmailChange, Invitation,
60
    Resource, PendingThirdPartyUser, get_latest_terms, RESOURCE_SEPARATOR,
61
    ProjectApplication, Project)
62
from astakos.im.settings import (
63
    INVITATIONS_PER_LEVEL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY,
64
    RECAPTCHA_ENABLED, DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
65
    PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN,
66
    MODERATION_ENABLED, PROJECT_MEMBER_JOIN_POLICIES,
67
    PROJECT_MEMBER_LEAVE_POLICIES, EMAILCHANGE_ENABLED,
68
    RESOURCES_PRESENTATION_DATA)
69
from astakos.im.widgets import DummyWidget, RecaptchaWidget
70
from astakos.im.functions import (
71
    send_change_email, submit_application, accept_membership_checks)
72

    
73
from astakos.im.util import reserved_email, reserved_verified_email, \
74
                            get_query, model_to_dict
75
from astakos.im import auth_providers
76

    
77
import astakos.im.messages as astakos_messages
78

    
79
import logging
80
import hashlib
81
import recaptcha.client.captcha as captcha
82
import re
83

    
84
logger = logging.getLogger(__name__)
85

    
86
DOMAIN_VALUE_REGEX = re.compile(
87
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
88
    re.IGNORECASE)
89

    
90
class StoreUserMixin(object):
91

    
92
    def store_user(self, user, request):
93
        """
94
        WARNING: this should be wrapped inside a transactional view/method.
95
        """
96
        user.save()
97
        self.post_store_user(user, request)
98
        return user
99

    
100
    def post_store_user(self, user, request):
101
        """
102
        Interface method for descendant backends to be able to do stuff within
103
        the transaction enabled by store_user.
104
        """
105
        pass
106

    
107

    
108
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
109
    """
110
    Extends the built in UserCreationForm in several ways:
111

112
    * Adds email, first_name, last_name, recaptcha_challenge_field,
113
    * recaptcha_response_field field.
114
    * The username field isn't visible and it is assigned a generated id.
115
    * User created is not active.
116
    """
117
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
118
    recaptcha_response_field = forms.CharField(
119
        widget=RecaptchaWidget, label='')
120

    
121
    class Meta:
122
        model = AstakosUser
123
        fields = ("email", "first_name", "last_name",
124
                  "has_signed_terms", "has_signed_terms")
125

    
126
    def __init__(self, *args, **kwargs):
127
        """
128
        Changes the order of fields, and removes the username field.
129
        """
130
        request = kwargs.pop('request', None)
131
        if request:
132
            self.ip = request.META.get('REMOTE_ADDR',
133
                                       request.META.get('HTTP_X_REAL_IP', None))
134

    
135
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
136
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
137
                                'password1', 'password2']
138

    
139
        if RECAPTCHA_ENABLED:
140
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
141
                                         'recaptcha_response_field', ])
142
        if get_latest_terms():
143
            self.fields.keyOrder.append('has_signed_terms')
144

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

    
153
    def clean_email(self):
154
        email = self.cleaned_data['email']
155
        if not email:
156
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
157
        if reserved_verified_email(email):
158
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
159
        return email
160

    
161
    def clean_has_signed_terms(self):
162
        has_signed_terms = self.cleaned_data['has_signed_terms']
163
        if not has_signed_terms:
164
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
165
        return has_signed_terms
166

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

    
172
    def clean_recaptcha_challenge_field(self):
173
        if 'recaptcha_response_field' in self.cleaned_data:
174
            self.validate_captcha()
175
        return self.cleaned_data['recaptcha_challenge_field']
176

    
177
    def validate_captcha(self):
178
        rcf = self.cleaned_data['recaptcha_challenge_field']
179
        rrf = self.cleaned_data['recaptcha_response_field']
180
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
181
        if not check.is_valid:
182
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
183

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

    
192
    def save(self, commit=True):
193
        """
194
        Saves the email, first_name and last_name properties, after the normal
195
        save behavior is complete.
196
        """
197
        user = super(LocalUserCreationForm, self).save(commit=False)
198
        user.renew_token()
199
        if commit:
200
            user.save()
201
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
202
        return user
203

    
204

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

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

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

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

    
232

    
233
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
234
    id = forms.CharField(
235
        widget=forms.HiddenInput(),
236
        label='',
237
        required=False
238
    )
239
    third_party_identifier = forms.CharField(
240
        widget=forms.HiddenInput(),
241
        label=''
242
    )
243
    email = forms.EmailField(
244
        label='Contact email',
245
        help_text = 'This is needed for contact purposes. It doesn&#39;t need to be the same with the one you provided to login previously. '
246
    )
247

    
248
    class Meta:
249
        model = AstakosUser
250
        fields = ['id', 'email', 'third_party_identifier',
251
                  'first_name', 'last_name', 'has_signed_terms']
252

    
253
    def __init__(self, *args, **kwargs):
254
        """
255
        Changes the order of fields, and removes the username field.
256
        """
257
        self.request = kwargs.get('request', None)
258
        if self.request:
259
            kwargs.pop('request')
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 = auth_providers.get_provider(self.request.REQUEST.get('provider', 'local'))
280
            extra_message = _(astakos_messages.EXISTING_EMAIL_THIRD_PARTY_NOTIFICATION) % \
281
                    (provider.get_title_display, reverse('edit_profile'))
282

    
283
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED) + ' ' + \
284
                                        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 post_store_user(self, user, request):
294
        pending = PendingThirdPartyUser.objects.get(
295
                                token=request.POST.get('third_party_token'),
296
                                third_party_identifier= \
297
                            self.cleaned_data.get('third_party_identifier'))
298
        return user.add_pending_auth_provider(pending)
299

    
300
    def save(self, commit=True):
301
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
302
        user.set_unusable_password()
303
        user.renew_token()
304
        if commit:
305
            user.save()
306
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
307
        return user
308

    
309

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

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

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

    
334

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

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

    
347

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

    
352

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

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

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

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

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

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

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

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

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

    
403
        if username:
404
            try:
405
                user = AstakosUser.objects.get_by_identifier(username)
406
                if not user.has_auth_provider('local'):
407
                    provider = auth_providers.get_provider('local')
408
                    raise forms.ValidationError(
409
                        _(provider.get_message('NOT_ACTIVE_FOR_USER')))
410
            except AstakosUser.DoesNotExist:
411
                pass
412

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

    
425

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

432
    The class defines a save method which sets ``is_verified`` to True so as the
433
    user during the next login will not to be redirected to profile page.
434
    """
435
    renew = forms.BooleanField(label='Renew token', required=False)
436

    
437
    class Meta:
438
        model = AstakosUser
439
        fields = ('email', 'first_name', 'last_name', 'auth_token',
440
                  'auth_token_expires')
441

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

    
451
    def save(self, commit=True):
452
        user = super(ProfileForm, self).save(commit=False)
453
        user.is_verified = True
454
        if self.cleaned_data.get('renew'):
455
            user.renew_token(
456
                flush_sessions=True,
457
                current_key=self.session_key
458
            )
459
        if commit:
460
            user.save()
461
        return user
462

    
463

    
464

    
465
class FeedbackForm(forms.Form):
466
    """
467
    Form for writing feedback.
468
    """
469
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
470
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
471
                                    required=False)
472

    
473

    
474
class SendInvitationForm(forms.Form):
475
    """
476
    Form for sending an invitations
477
    """
478

    
479
    email = forms.EmailField(required=True, label='Email address')
480
    first_name = forms.EmailField(label='First name')
481
    last_name = forms.EmailField(label='Last name')
482

    
483

    
484
class ExtendedPasswordResetForm(PasswordResetForm):
485
    """
486
    Extends PasswordResetForm by overriding
487

488
    save method: to pass a custom from_email in send_mail.
489
    clean_email: to handle local auth provider checks
490
    """
491
    def clean_email(self):
492
        email = super(ExtendedPasswordResetForm, self).clean_email()
493
        try:
494
            user = AstakosUser.objects.get_by_identifier(email)
495

    
496
            if not user.is_active:
497
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
498

    
499
            if not user.has_usable_password():
500
                provider = auth_providers.get_provider('local')
501
                available_providers = user.auth_providers.all()
502
                available_providers = ",".join(p.settings.get_title_display for p in \
503
                                                   available_providers)
504
                message = astakos_messages.UNUSABLE_PASSWORD % \
505
                    (provider.get_method_prompt_display, available_providers)
506
                raise forms.ValidationError(message)
507

    
508
            if not user.can_change_password():
509
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
510
        except AstakosUser.DoesNotExist, e:
511
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
512
        return email
513

    
514
    def save(
515
        self, domain_override=None, email_template_name='registration/password_reset_email.html',
516
            use_https=False, token_generator=default_token_generator, request=None):
517
        """
518
        Generates a one-use only link for resetting password and sends to the user.
519
        """
520
        for user in self.users_cache:
521
            url = user.astakosuser.get_password_reset_url(token_generator)
522
            url = urljoin(BASEURL, url)
523
            t = loader.get_template(email_template_name)
524
            c = {
525
                'email': user.email,
526
                'url': url,
527
                'site_name': SITENAME,
528
                'user': user,
529
                'baseurl': BASEURL,
530
                'support': DEFAULT_CONTACT_EMAIL
531
            }
532
            from_email = settings.SERVER_EMAIL
533
            send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
534
                      t.render(Context(c)),
535
                      from_email,
536
                      [user.email],
537
                      connection=get_connection())
538

    
539

    
540
class EmailChangeForm(forms.ModelForm):
541

    
542
    class Meta:
543
        model = EmailChange
544
        fields = ('new_email_address',)
545

    
546
    def clean_new_email_address(self):
547
        addr = self.cleaned_data['new_email_address']
548
        if reserved_verified_email(addr):
549
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
550
        return addr
551

    
552
    def save(self, request, email_template_name='registration/email_change_email.txt', commit=True):
553
        ec = super(EmailChangeForm, self).save(commit=False)
554
        ec.user = request.user
555
        # delete pending email changes
556
        request.user.emailchanges.all().delete()
557

    
558
        activation_key = hashlib.sha1(
559
            str(random()) + smart_str(ec.new_email_address))
560
        ec.activation_key = activation_key.hexdigest()
561
        if commit:
562
            ec.save()
563
        send_change_email(ec, request, email_template_name=email_template_name)
564

    
565

    
566
class SignApprovalTermsForm(forms.ModelForm):
567

    
568
    class Meta:
569
        model = AstakosUser
570
        fields = ("has_signed_terms",)
571

    
572
    def __init__(self, *args, **kwargs):
573
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
574

    
575
    def clean_has_signed_terms(self):
576
        has_signed_terms = self.cleaned_data['has_signed_terms']
577
        if not has_signed_terms:
578
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
579
        return has_signed_terms
580

    
581

    
582
class InvitationForm(forms.ModelForm):
583

    
584
    username = forms.EmailField(label=_("Email"))
585

    
586
    def __init__(self, *args, **kwargs):
587
        super(InvitationForm, self).__init__(*args, **kwargs)
588

    
589
    class Meta:
590
        model = Invitation
591
        fields = ('username', 'realname')
592

    
593
    def clean_username(self):
594
        username = self.cleaned_data['username']
595
        try:
596
            Invitation.objects.get(username=username)
597
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
598
        except Invitation.DoesNotExist:
599
            pass
600
        return username
601

    
602

    
603
class ExtendedPasswordChangeForm(PasswordChangeForm):
604
    """
605
    Extends PasswordChangeForm by enabling user
606
    to optionally renew also the token.
607
    """
608
    if not NEWPASSWD_INVALIDATE_TOKEN:
609
        renew = forms.BooleanField(label='Renew token', required=False,
610
                                   initial=True,
611
                                   help_text='Unsetting this may result in security risk.')
612

    
613
    def __init__(self, user, *args, **kwargs):
614
        self.session_key = kwargs.pop('session_key', None)
615
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
616

    
617
    def save(self, commit=True):
618
        try:
619
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
620
                self.user.renew_token()
621
            self.user.flush_sessions(current_key=self.session_key)
622
        except AttributeError:
623
            # if user model does has not such methods
624
            pass
625
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
626

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

    
639
    def __init__(self, user, *args, **kwargs):
640
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
641

    
642
    @transaction.commit_on_success()
643
    def save(self, commit=True):
644
        try:
645
            self.user = AstakosUser.objects.get(id=self.user.id)
646
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
647
                self.user.renew_token()
648
            #self.user.flush_sessions()
649
            if not self.user.has_auth_provider('local'):
650
                self.user.add_auth_provider('local', auth_backend='astakos')
651

    
652
        except BaseException, e:
653
            logger.exception(e)
654
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
655

    
656

    
657

    
658

    
659
app_name_label       =  "Project name"
660
app_name_placeholder = _("myproject.mylab.ntua.gr")
661
app_name_validator   =  validators.RegexValidator(
662
                            DOMAIN_VALUE_REGEX,
663
                            _(astakos_messages.DOMAIN_VALUE_ERR),
664
                            'invalid')
665
app_name_help        =  _("""
666
        The Project's name should be in a domain format.
667
        The domain shouldn't neccessarily exist in the real
668
        world but is helpful to imply a structure.
669
        e.g.: myproject.mylab.ntua.gr or
670
        myservice.myteam.myorganization""")
671
app_name_widget      =  forms.TextInput(
672
                            attrs={'placeholder': app_name_placeholder})
673

    
674

    
675
app_home_label       =  "Homepage URL"
676
app_home_placeholder =  'myinstitution.org/myproject/'
677
app_home_help        =  _("""
678
        URL pointing at your project's site.
679
        e.g.: myinstitution.org/myproject/.
680
        Leave blank if there is no website.""")
681
app_home_widget      =  forms.TextInput(
682
                            attrs={'placeholder': app_home_placeholder})
683

    
684
app_desc_label       =  _("Description")
685
app_desc_help        =  _("""
686
        Please provide a short but descriptive abstract of your Project,
687
        so that anyone searching can quickly understand
688
        what this Project is about.""")
689

    
690
app_comment_label    =  _("Comments for review (private)")
691
app_comment_help     =  _("""
692
        Write down any comments you may have for the reviewer
693
        of this application (e.g. background and rationale to
694
        support your request).
695
        The comments are strictly for the review process
696
        and will not be published.""")
697

    
698
app_start_date_label =  _("Start date")
699
app_start_date_help  =  _("""
700
        Provide a date when your need your project to be created,
701
        and members to be able to join and get resources.
702
        This date is only a hint to help prioritize reviews.""")
703

    
704
app_end_date_label   =  _("Termination date")
705
app_end_date_help    =  _("""
706
        At this date, the project will be automatically terminated
707
        and its resource grants revoked from all members.
708
        Unless you know otherwise,
709
        it is best to start with a conservative estimation.
710
        You can always re-apply for an extension, if you need.""")
711

    
712
join_policy_label    =  _("Joining policy")
713
app_member_join_policy_help    =  _("""
714
        Text fo member_join_policy.""")
715
leave_policy_label   =  _("Leaving policy")
716
app_member_leave_policy_help    =  _("""
717
        Text fo member_leave_policy.""")
718

    
719
max_members_label    =  _("Maximum member count")
720
max_members_help     =  _("""
721
        Specify the maximum number of members this project may have,
722
        including the owner. Beyond this number, no new members
723
        may join the project and be granted the project resources.
724
        Unless you certainly for otherwise,
725
        it is best to start with a conservative limit.
726
        You can always request a raise when you need it.""")
727

    
728
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
729
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
730

    
731
class ProjectApplicationForm(forms.ModelForm):
732

    
733
    name = forms.CharField(
734
        label     = app_name_label,
735
        help_text = app_name_help,
736
        widget    = app_name_widget,
737
        validators = [app_name_validator])
738

    
739
    homepage = forms.URLField(
740
        label     = app_home_label,
741
        help_text = app_home_help,
742
        widget    = app_home_widget,
743
        required  = False)
744

    
745
    description = forms.CharField(
746
        label     = app_desc_label,
747
        help_text = app_desc_help,
748
        widget    = forms.Textarea,
749
        required  = False)
750

    
751
    comments = forms.CharField(
752
        label     = app_comment_label,
753
        help_text = app_comment_help,
754
        widget    = forms.Textarea,
755
        required  = False)
756

    
757
    start_date = forms.DateTimeField(
758
        label     = app_start_date_label,
759
        help_text = app_start_date_help,
760
        required  = False)
761

    
762
    end_date = forms.DateTimeField(
763
        label     = app_end_date_label,
764
        help_text = app_end_date_help)
765

    
766
    member_join_policy  = forms.TypedChoiceField(
767
        label     = join_policy_label,
768
        help_text = app_member_join_policy_help,
769
        initial   = 2,
770
        coerce    = int,
771
        choices   = join_policies)
772

    
773
    member_leave_policy = forms.TypedChoiceField(
774
        label     = leave_policy_label,
775
        help_text = app_member_leave_policy_help,
776
        coerce    = int,
777
        choices   = leave_policies)
778

    
779
    limit_on_members_number = forms.IntegerField(
780
        label     = max_members_label,
781
        help_text = max_members_help,
782
        required  = False)
783

    
784
    class Meta:
785
        model = ProjectApplication
786
        fields = ( 'name', 'homepage', 'description',
787
                    'start_date', 'end_date', 'comments',
788
                    'member_join_policy', 'member_leave_policy',
789
                    'limit_on_members_number')
790

    
791
    def __init__(self, *args, **kwargs):
792
        instance = kwargs.get('instance')
793
        self.precursor_application = instance
794
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
795
        # in case of new application remove closed join policy
796
        if not instance:
797
            policies = PROJECT_MEMBER_JOIN_POLICIES.copy()
798
            policies.pop('3')
799
            self.fields['member_join_policy'].choices = policies.iteritems()
800

    
801
    def clean_start_date(self):
802
        start_date = self.cleaned_data.get('start_date')
803
        if not self.precursor_application:
804
            today = datetime.now()
805
            today = datetime(today.year, today.month, today.day)
806
            if start_date and (start_date - today).days < 0:
807
                raise forms.ValidationError(
808
                _(astakos_messages.INVALID_PROJECT_START_DATE))
809
        return start_date
810

    
811
    def clean_end_date(self):
812
        start_date = self.cleaned_data.get('start_date')
813
        end_date = self.cleaned_data.get('end_date')
814
        today = datetime.now()
815
        today = datetime(today.year, today.month, today.day)
816
        if end_date and (end_date - today).days < 0:
817
            raise forms.ValidationError(
818
                _(astakos_messages.INVALID_PROJECT_END_DATE))
819
        if start_date and (end_date - start_date).days <= 0:
820
            raise forms.ValidationError(
821
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
822
        return end_date
823

    
824
    def clean(self):
825
        userid = self.data.get('user', None)
826
        self.user = None
827
        if userid:
828
            try:
829
                self.user = AstakosUser.objects.get(id=userid)
830
            except AstakosUser.DoesNotExist:
831
                pass
832
        if not self.user:
833
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
834
        super(ProjectApplicationForm, self).clean()
835
        return self.cleaned_data
836

    
837
    @property
838
    def resource_policies(self):
839
        policies = []
840
        append = policies.append
841
        for name, value in self.data.iteritems():
842
            if not value:
843
                continue
844
            uplimit = value
845
            if name.endswith('_uplimit'):
846
                subs = name.split('_uplimit')
847
                prefix, suffix = subs
848
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
849
                resource = Resource.objects.get(service__name=s, name=r)
850

    
851
                # keep only resource limits for selected resource groups
852
                if self.data.get(
853
                    'is_selected_%s' % resource.group, "0"
854
                 ) == "1":
855
                    d = model_to_dict(resource)
856
                    if uplimit:
857
                        d.update(dict(service=s, resource=r, uplimit=uplimit))
858
                    else:
859
                        d.update(dict(service=s, resource=r, uplimit=None))
860
                    append(d)
861

    
862
        ordered_keys = RESOURCES_PRESENTATION_DATA['resources_order']
863
        policies = sorted(policies, key=lambda r:ordered_keys.index(r['str_repr']))
864
        return policies
865

    
866
    def save(self, commit=True):
867
        data = dict(self.cleaned_data)
868
        data['precursor_application'] = self.instance.id
869
        data['owner'] = self.user
870
        data['resource_policies'] = self.resource_policies
871
        submit_application(data, request_user=self.user)
872

    
873
class ProjectSortForm(forms.Form):
874
    sorting = forms.ChoiceField(
875
        label='Sort by',
876
        choices=(('name', 'Sort by Name'),
877
                 ('issue_date', 'Sort by Issue date'),
878
                 ('start_date', 'Sort by Start Date'),
879
                 ('end_date', 'Sort by End Date'),
880
#                  ('approved_members_num', 'Sort by Participants'),
881
                 ('state', 'Sort by Status'),
882
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
883
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
884
                 ('-name', 'Sort by Name'),
885
                 ('-issue_date', 'Sort by Issue date'),
886
                 ('-start_date', 'Sort by Start Date'),
887
                 ('-end_date', 'Sort by End Date'),
888
#                  ('-approved_members_num', 'Sort by Participants'),
889
                 ('-state', 'Sort by Status'),
890
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
891
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
892
        ),
893
        required=True
894
    )
895

    
896
class AddProjectMembersForm(forms.Form):
897
    q = forms.CharField(
898
        max_length=800, widget=forms.Textarea, label=_('Add members'),
899
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
900

    
901
    def __init__(self, *args, **kwargs):
902
        chain_id = kwargs.pop('chain_id', None)
903
        if chain_id:
904
            self.project = Project.objects.get(id=chain_id)
905
        self.request_user = kwargs.pop('request_user', None)
906
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
907

    
908
    def clean(self):
909
        try:
910
            accept_membership_checks(self.project, self.request_user)
911
        except PermissionDenied, e:
912
            raise forms.ValidationError(e)
913

    
914
        q = self.cleaned_data.get('q') or ''
915
        users = q.split(',')
916
        users = list(u.strip() for u in users if u)
917
        db_entries = AstakosUser.objects.filter(email__in=users)
918
        unknown = list(set(users) - set(u.email for u in db_entries))
919
        if unknown:
920
            raise forms.ValidationError(
921
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
922
        self.valid_users = db_entries
923
        return self.cleaned_data
924

    
925
    def get_valid_users(self):
926
        """Should be called after form cleaning"""
927
        try:
928
            return self.valid_users
929
        except:
930
            return ()
931

    
932
class ProjectMembersSortForm(forms.Form):
933
    sorting = forms.ChoiceField(
934
        label='Sort by',
935
        choices=(('person__email', 'User Id'),
936
                 ('person__first_name', 'Name'),
937
                 ('acceptance_date', 'Acceptance date')
938
        ),
939
        required=True
940
    )
941

    
942

    
943
class ProjectSearchForm(forms.Form):
944
    q = forms.CharField(max_length=200, label='Search project', required=False)
945

    
946

    
947
class ExtendedProfileForm(ProfileForm):
948
    """
949
    Profile form that combines `email change` and `password change` user
950
    actions by propagating submited data to internal EmailChangeForm
951
    and ExtendedPasswordChangeForm objects.
952
    """
953

    
954
    password_change_form = None
955
    email_change_form = None
956

    
957
    password_change = False
958
    email_change = False
959

    
960
    extra_forms_fields = {
961
        'email': ['new_email_address'],
962
        'password': ['old_password', 'new_password1', 'new_password2']
963
    }
964

    
965
    fields = ('email')
966
    change_password = forms.BooleanField(initial=False, required=False)
967
    change_email = forms.BooleanField(initial=False, required=False)
968

    
969
    email_changed = False
970
    password_changed = False
971

    
972
    def __init__(self, *args, **kwargs):
973
        session_key = kwargs.get('session_key', None)
974
        self.fields_list = [
975
                'email',
976
                'new_email_address',
977
                'first_name',
978
                'last_name',
979
                'auth_token',
980
                'auth_token_expires',
981
                'old_password',
982
                'new_password1',
983
                'new_password2',
984
                'change_email',
985
                'change_password',
986
        ]
987

    
988
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
989
        self.session_key = session_key
990
        if self.instance.can_change_password():
991
            self.password_change = True
992
        else:
993
            self.fields_list.remove('old_password')
994
            self.fields_list.remove('new_password1')
995
            self.fields_list.remove('new_password2')
996
            self.fields_list.remove('change_password')
997
            del self.fields['change_password']
998

    
999

    
1000
        if EMAILCHANGE_ENABLED and self.instance.can_change_email():
1001
            self.email_change = True
1002
        else:
1003
            self.fields_list.remove('new_email_address')
1004
            self.fields_list.remove('change_email')
1005
            del self.fields['change_email']
1006

    
1007
        self._init_extra_forms()
1008
        self.save_extra_forms = []
1009
        self.success_messages = []
1010
        self.fields.keyOrder = self.fields_list
1011

    
1012

    
1013
    def _init_extra_form_fields(self):
1014
        if self.email_change:
1015
            self.fields.update(self.email_change_form.fields)
1016
            self.fields['new_email_address'].required = False
1017

    
1018
        if self.password_change:
1019
            self.fields.update(self.password_change_form.fields)
1020
            self.fields['old_password'].required = False
1021
            self.fields['old_password'].label = _('Password')
1022
            self.fields['old_password'].initial = 'password'
1023
            self.fields['new_password1'].required = False
1024
            self.fields['new_password2'].required = False
1025

    
1026
    def _update_extra_form_errors(self):
1027
        if self.cleaned_data.get('change_password'):
1028
            self.errors.update(self.password_change_form.errors)
1029
        if self.cleaned_data.get('change_email'):
1030
            self.errors.update(self.email_change_form.errors)
1031

    
1032
    def _init_extra_forms(self):
1033
        self.email_change_form = EmailChangeForm(self.data)
1034
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1035
                                   data=self.data, session_key=self.session_key)
1036
        self._init_extra_form_fields()
1037

    
1038
    def is_valid(self):
1039
        password, email = True, True
1040
        profile = super(ExtendedProfileForm, self).is_valid()
1041
        if profile and self.cleaned_data.get('change_password', None):
1042

    
1043
            password = self.password_change_form.is_valid()
1044
            self.save_extra_forms.append('password')
1045
        if profile and self.cleaned_data.get('change_email'):
1046
            self.fields['new_email_address'].required = True
1047
            email = self.email_change_form.is_valid()
1048
            self.save_extra_forms.append('email')
1049

    
1050
        if not password or not email:
1051
            self._update_extra_form_errors()
1052

    
1053
        return all([profile, password, email])
1054

    
1055
    def save(self, request, *args, **kwargs):
1056
        if 'email' in self.save_extra_forms:
1057
            self.email_change_form.save(request, *args, **kwargs)
1058
            self.email_changed = True
1059
        if 'password' in self.save_extra_forms:
1060
            self.password_change_form.save(*args, **kwargs)
1061
            self.password_changed = True
1062
        return super(ExtendedProfileForm, self).save(*args, **kwargs)
1063