Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (39.6 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. ' \
246
        'It doesn&#39;t need to be the same with the one you ' \
247
        'provided to login previously. '
248
    )
249

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

    
255
    def __init__(self, *args, **kwargs):
256
        """
257
        Changes the order of fields, and removes the username field.
258
        """
259
        self.request = kwargs.get('request', None)
260
        if self.request:
261
            kwargs.pop('request')
262

    
263
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
264

    
265
        if not get_latest_terms():
266
            del self.fields['has_signed_terms']
267

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

    
276
    def clean_email(self):
277
        email = self.cleaned_data['email']
278
        if not email:
279
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
280
        if reserved_verified_email(email):
281
            provider = auth_providers.get_provider(self.request.REQUEST.get('provider', 'local'))
282
            extra_message = _(astakos_messages.EXISTING_EMAIL_THIRD_PARTY_NOTIFICATION) % \
283
                    (provider.get_title_display, reverse('edit_profile'))
284

    
285
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED) + ' ' + \
286
                                        extra_message)
287
        return email
288

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

    
295
    def post_store_user(self, user, request):
296
        pending = PendingThirdPartyUser.objects.get(
297
                                token=request.POST.get('third_party_token'),
298
                                third_party_identifier= \
299
                            self.cleaned_data.get('third_party_identifier'))
300
        return user.add_pending_auth_provider(pending)
301

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

    
311

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

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

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

    
336

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

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

    
349

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

    
354

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

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

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

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

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

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

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

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

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

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

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

    
427

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

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

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

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

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

    
467

    
468

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

    
477

    
478
class SendInvitationForm(forms.Form):
479
    """
480
    Form for sending an invitations
481
    """
482

    
483
    email = forms.EmailField(required=True, label='Email address')
484
    first_name = forms.EmailField(label='First name')
485
    last_name = forms.EmailField(label='Last name')
486

    
487

    
488
class ExtendedPasswordResetForm(PasswordResetForm):
489
    """
490
    Extends PasswordResetForm by overriding
491

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

    
500
            if not user.is_active:
501
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
502

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

    
512
            if not user.can_change_password():
513
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
514
        except AstakosUser.DoesNotExist, e:
515
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
516
        return email
517

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

    
543

    
544
class EmailChangeForm(forms.ModelForm):
545

    
546
    class Meta:
547
        model = EmailChange
548
        fields = ('new_email_address',)
549

    
550
    def clean_new_email_address(self):
551
        addr = self.cleaned_data['new_email_address']
552
        if reserved_verified_email(addr):
553
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
554
        return addr
555

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

    
562
        activation_key = hashlib.sha1(
563
            str(random()) + smart_str(ec.new_email_address))
564
        ec.activation_key = activation_key.hexdigest()
565
        if commit:
566
            ec.save()
567
        send_change_email(ec, request, email_template_name=email_template_name)
568

    
569

    
570
class SignApprovalTermsForm(forms.ModelForm):
571

    
572
    class Meta:
573
        model = AstakosUser
574
        fields = ("has_signed_terms",)
575

    
576
    def __init__(self, *args, **kwargs):
577
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
578

    
579
    def clean_has_signed_terms(self):
580
        has_signed_terms = self.cleaned_data['has_signed_terms']
581
        if not has_signed_terms:
582
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
583
        return has_signed_terms
584

    
585

    
586
class InvitationForm(forms.ModelForm):
587

    
588
    username = forms.EmailField(label=_("Email"))
589

    
590
    def __init__(self, *args, **kwargs):
591
        super(InvitationForm, self).__init__(*args, **kwargs)
592

    
593
    class Meta:
594
        model = Invitation
595
        fields = ('username', 'realname')
596

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

    
606

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

    
617
    def __init__(self, user, *args, **kwargs):
618
        self.session_key = kwargs.pop('session_key', None)
619
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
620

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

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

    
643
    def __init__(self, user, *args, **kwargs):
644
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
645

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

    
656
        except BaseException, e:
657
            logger.exception(e)
658
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
659

    
660

    
661

    
662

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

    
678

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

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

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

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

    
708
app_end_date_label   =  _("Termination date")
709
app_end_date_help    =  _("""
710
        At this date, the project will be automatically terminated
711
        and its resource grants revoked from all members. If you are
712
        not certain, it is best to start with a conservative estimation.
713
        You can always re-apply for an extension, if you need.""")
714

    
715
join_policy_label    =  _("Joining policy")
716
app_member_join_policy_help    =  _("""
717
        Select how new members are accepted into the project.""")
718
leave_policy_label   =  _("Leaving policy")
719
app_member_leave_policy_help    =  _("""
720
        Select how new members can leave the project.""")
721

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

    
730
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
731
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
732

    
733
class ProjectApplicationForm(forms.ModelForm):
734

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

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

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

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

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

    
764
    end_date = forms.DateTimeField(
765
        label     = app_end_date_label,
766
        help_text = app_end_date_help)
767

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

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

    
781
    limit_on_members_number = forms.IntegerField(
782
        label     = max_members_label,
783
        help_text = max_members_help,
784
        min_value = 0,
785
        required  = False)
786

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

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

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

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

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

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

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

    
865
        ordered_keys = RESOURCES_PRESENTATION_DATA['resources_order']
866
        policies = sorted(policies, key=lambda r:ordered_keys.index(r['str_repr']))
867
        return policies
868

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

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

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

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

    
911
    def clean(self):
912
        try:
913
            accept_membership_checks(self.project, self.request_user)
914
        except PermissionDenied, e:
915
            raise forms.ValidationError(e)
916

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

    
928
    def get_valid_users(self):
929
        """Should be called after form cleaning"""
930
        try:
931
            return self.valid_users
932
        except:
933
            return ()
934

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

    
945

    
946
class ProjectSearchForm(forms.Form):
947
    q = forms.CharField(max_length=200, label='Search project', required=False)
948

    
949

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

    
957
    password_change_form = None
958
    email_change_form = None
959

    
960
    password_change = False
961
    email_change = False
962

    
963
    extra_forms_fields = {
964
        'email': ['new_email_address'],
965
        'password': ['old_password', 'new_password1', 'new_password2']
966
    }
967

    
968
    fields = ('email')
969
    change_password = forms.BooleanField(initial=False, required=False)
970
    change_email = forms.BooleanField(initial=False, required=False)
971

    
972
    email_changed = False
973
    password_changed = False
974

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

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

    
1003

    
1004
        if EMAILCHANGE_ENABLED and self.instance.can_change_email():
1005
            self.email_change = True
1006
        else:
1007
            self.fields_list.remove('new_email_address')
1008
            self.fields_list.remove('change_email')
1009
            del self.fields['change_email']
1010

    
1011
        self._init_extra_forms()
1012
        self.save_extra_forms = []
1013
        self.success_messages = []
1014
        self.fields.keyOrder = self.fields_list
1015

    
1016

    
1017
    def _init_extra_form_fields(self):
1018
        if self.email_change:
1019
            self.fields.update(self.email_change_form.fields)
1020
            self.fields['new_email_address'].required = False
1021
            self.fields['email'].help_text = _('Request email change')
1022

    
1023
        if self.password_change:
1024
            self.fields.update(self.password_change_form.fields)
1025
            self.fields['old_password'].required = False
1026
            self.fields['old_password'].label = _('Password')
1027
            self.fields['old_password'].help_text = _('Change your local '
1028
                                                      'password')
1029
            self.fields['old_password'].initial = 'password'
1030
            self.fields['new_password1'].required = False
1031
            self.fields['new_password2'].required = False
1032

    
1033
    def _update_extra_form_errors(self):
1034
        if self.cleaned_data.get('change_password'):
1035
            self.errors.update(self.password_change_form.errors)
1036
        if self.cleaned_data.get('change_email'):
1037
            self.errors.update(self.email_change_form.errors)
1038

    
1039
    def _init_extra_forms(self):
1040
        self.email_change_form = EmailChangeForm(self.data)
1041
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1042
                                   data=self.data, session_key=self.session_key)
1043
        self._init_extra_form_fields()
1044

    
1045
    def is_valid(self):
1046
        password, email = True, True
1047
        profile = super(ExtendedProfileForm, self).is_valid()
1048
        if profile and self.cleaned_data.get('change_password', None):
1049

    
1050
            password = self.password_change_form.is_valid()
1051
            self.save_extra_forms.append('password')
1052
        if profile and self.cleaned_data.get('change_email'):
1053
            self.fields['new_email_address'].required = True
1054
            email = self.email_change_form.is_valid()
1055
            self.save_extra_forms.append('email')
1056

    
1057
        if not password or not email:
1058
            self._update_extra_form_errors()
1059

    
1060
        return all([profile, password, email])
1061

    
1062
    def save(self, request, *args, **kwargs):
1063
        if 'email' in self.save_extra_forms:
1064
            self.email_change_form.save(request, *args, **kwargs)
1065
            self.email_changed = True
1066
        if 'password' in self.save_extra_forms:
1067
            self.password_change_form.save(*args, **kwargs)
1068
            self.password_changed = True
1069
        return super(ExtendedProfileForm, self).save(*args, **kwargs)
1070