Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (39.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 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
    uuid = forms.CharField(label='User id', required=False)
437

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

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

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

    
464

    
465

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

    
474

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

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

    
484

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

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

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

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

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

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

    
540

    
541
class EmailChangeForm(forms.ModelForm):
542

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

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

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

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

    
566

    
567
class SignApprovalTermsForm(forms.ModelForm):
568

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

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

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

    
582

    
583
class InvitationForm(forms.ModelForm):
584

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

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

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

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

    
603

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

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

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

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

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

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

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

    
657

    
658

    
659

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

    
675

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

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

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

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

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

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

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

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

    
732
class ProjectApplicationForm(forms.ModelForm):
733

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
943

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

    
947

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

    
955
    password_change_form = None
956
    email_change_form = None
957

    
958
    password_change = False
959
    email_change = False
960

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

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

    
970
    email_changed = False
971
    password_changed = False
972

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

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

    
1001

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

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

    
1014

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

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

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

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

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

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

    
1052
        if not password or not email:
1053
            self._update_extra_form_errors()
1054

    
1055
        return all([profile, password, email])
1056

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