Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 97e93991

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
        min_value = 0,
784
        required  = False)
785

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
944

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

    
948

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

    
956
    password_change_form = None
957
    email_change_form = None
958

    
959
    password_change = False
960
    email_change = False
961

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

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

    
971
    email_changed = False
972
    password_changed = False
973

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

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

    
1002

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

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

    
1015

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

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

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

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

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

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

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

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

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