Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 31bc3a62

History | View | Annotate | Download (40.1 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, 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 clean_email(self):
456
        return self.instance.email
457

    
458
    def clean_auth_token(self):
459
        return self.instance.auth_token
460

    
461
    def clean_auth_token_expires(self):
462
        return self.instance.auth_token_expires
463

    
464
    def clean_uuid(self):
465
        return self.instance.uuid
466

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

    
479

    
480

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

    
489

    
490
class SendInvitationForm(forms.Form):
491
    """
492
    Form for sending an invitations
493
    """
494

    
495
    email = forms.EmailField(required=True, label='Email address')
496
    first_name = forms.EmailField(label='First name')
497
    last_name = forms.EmailField(label='Last name')
498

    
499

    
500
class ExtendedPasswordResetForm(PasswordResetForm):
501
    """
502
    Extends PasswordResetForm by overriding
503

504
    save method: to pass a custom from_email in send_mail.
505
    clean_email: to handle local auth provider checks
506
    """
507
    def clean_email(self):
508
        email = super(ExtendedPasswordResetForm, self).clean_email()
509
        try:
510
            user = AstakosUser.objects.get_by_identifier(email)
511

    
512
            if not user.is_active:
513
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
514

    
515
            if not user.has_usable_password():
516
                provider = auth_providers.get_provider('local')
517
                available_providers = user.auth_providers.all()
518
                available_providers = ",".join(p.settings.get_title_display for p in \
519
                                                   available_providers)
520
                message = astakos_messages.UNUSABLE_PASSWORD % \
521
                    (provider.get_method_prompt_display, available_providers)
522
                raise forms.ValidationError(message)
523

    
524
            if not user.can_change_password():
525
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
526
        except AstakosUser.DoesNotExist, e:
527
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
528
        return email
529

    
530
    def save(
531
        self, domain_override=None, email_template_name='registration/password_reset_email.html',
532
            use_https=False, token_generator=default_token_generator, request=None):
533
        """
534
        Generates a one-use only link for resetting password and sends to the user.
535
        """
536
        for user in self.users_cache:
537
            url = user.astakosuser.get_password_reset_url(token_generator)
538
            url = urljoin(BASEURL, url)
539
            t = loader.get_template(email_template_name)
540
            c = {
541
                'email': user.email,
542
                'url': url,
543
                'site_name': SITENAME,
544
                'user': user,
545
                'baseurl': BASEURL,
546
                'support': CONTACT_EMAIL
547
            }
548
            from_email = settings.SERVER_EMAIL
549
            send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
550
                      t.render(Context(c)),
551
                      from_email,
552
                      [user.email],
553
                      connection=get_connection())
554

    
555

    
556
class EmailChangeForm(forms.ModelForm):
557

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

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

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

    
574
        activation_key = hashlib.sha1(
575
            str(random()) + smart_str(ec.new_email_address))
576
        ec.activation_key = activation_key.hexdigest()
577
        if commit:
578
            ec.save()
579
        send_change_email(ec, request, email_template_name=email_template_name)
580

    
581

    
582
class SignApprovalTermsForm(forms.ModelForm):
583

    
584
    class Meta:
585
        model = AstakosUser
586
        fields = ("has_signed_terms",)
587

    
588
    def __init__(self, *args, **kwargs):
589
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
590

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

    
597

    
598
class InvitationForm(forms.ModelForm):
599

    
600
    username = forms.EmailField(label=_("Email"))
601

    
602
    def __init__(self, *args, **kwargs):
603
        super(InvitationForm, self).__init__(*args, **kwargs)
604

    
605
    class Meta:
606
        model = Invitation
607
        fields = ('username', 'realname')
608

    
609
    def clean_username(self):
610
        username = self.cleaned_data['username']
611
        try:
612
            Invitation.objects.get(username=username)
613
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
614
        except Invitation.DoesNotExist:
615
            pass
616
        return username
617

    
618

    
619
class ExtendedPasswordChangeForm(PasswordChangeForm):
620
    """
621
    Extends PasswordChangeForm by enabling user
622
    to optionally renew also the token.
623
    """
624
    if not NEWPASSWD_INVALIDATE_TOKEN:
625
        renew = forms.BooleanField(label='Renew token', required=False,
626
                                   initial=True,
627
                                   help_text='Unsetting this may result in security risk.')
628

    
629
    def __init__(self, user, *args, **kwargs):
630
        self.session_key = kwargs.pop('session_key', None)
631
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
632

    
633
    def save(self, commit=True):
634
        try:
635
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
636
                self.user.renew_token()
637
            self.user.flush_sessions(current_key=self.session_key)
638
        except AttributeError:
639
            # if user model does has not such methods
640
            pass
641
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
642

    
643
class ExtendedSetPasswordForm(SetPasswordForm):
644
    """
645
    Extends SetPasswordForm by enabling user
646
    to optionally renew also the token.
647
    """
648
    if not NEWPASSWD_INVALIDATE_TOKEN:
649
        renew = forms.BooleanField(
650
            label='Renew token',
651
            required=False,
652
            initial=True,
653
            help_text='Unsetting this may result in security risk.')
654

    
655
    def __init__(self, user, *args, **kwargs):
656
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
657

    
658
    @transaction.commit_on_success()
659
    def save(self, commit=True):
660
        try:
661
            self.user = AstakosUser.objects.get(id=self.user.id)
662
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
663
                self.user.renew_token()
664
            #self.user.flush_sessions()
665
            if not self.user.has_auth_provider('local'):
666
                self.user.add_auth_provider('local', auth_backend='astakos')
667

    
668
        except BaseException, e:
669
            logger.exception(e)
670
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
671

    
672

    
673

    
674

    
675
app_name_label       =  "Project name"
676
app_name_placeholder = _("myproject.mylab.ntua.gr")
677
app_name_validator   =  validators.RegexValidator(
678
                            DOMAIN_VALUE_REGEX,
679
                            _(astakos_messages.DOMAIN_VALUE_ERR),
680
                            'invalid')
681
app_name_help        =  _("""
682
        The project's name should be in a domain format.
683
        The domain shouldn't neccessarily exist in the real
684
        world but is helpful to imply a structure.
685
        e.g.: myproject.mylab.ntua.gr or
686
        myservice.myteam.myorganization""")
687
app_name_widget      =  forms.TextInput(
688
                            attrs={'placeholder': app_name_placeholder})
689

    
690

    
691
app_home_label       =  "Homepage URL"
692
app_home_placeholder =  'myinstitution.org/myproject/'
693
app_home_help        =  _("""
694
        URL pointing at your project's site.
695
        e.g.: myinstitution.org/myproject/.
696
        Leave blank if there is no website.""")
697
app_home_widget      =  forms.TextInput(
698
                            attrs={'placeholder': app_home_placeholder})
699

    
700
app_desc_label       =  _("Description")
701
app_desc_help        =  _("""
702
        Please provide a short but descriptive abstract of your
703
        project, so that anyone searching can quickly understand
704
        what this project is about.""")
705

    
706
app_comment_label    =  _("Comments for review (private)")
707
app_comment_help     =  _("""
708
        Write down any comments you may have for the reviewer
709
        of this application (e.g. background and rationale to
710
        support your request).
711
        The comments are strictly for the review process
712
        and will not be made public.""")
713

    
714
app_start_date_label =  _("Start date")
715
app_start_date_help  =  _("""
716
        Provide a date when your need your project to be created,
717
        and members to be able to join and get resources.
718
        This date is only a hint to help prioritize reviews.""")
719

    
720
app_end_date_label   =  _("Termination date")
721
app_end_date_help    =  _("""
722
        At this date, the project will be automatically terminated
723
        and its resource grants revoked from all members. If you are
724
        not certain, it is best to start with a conservative estimation.
725
        You can always re-apply for an extension, if you need.""")
726

    
727
join_policy_label    =  _("Joining policy")
728
app_member_join_policy_help    =  _("""
729
        Select how new members are accepted into the project.""")
730
leave_policy_label   =  _("Leaving policy")
731
app_member_leave_policy_help    =  _("""
732
        Select how new members can leave the project.""")
733

    
734
max_members_label    =  _("Maximum member count")
735
max_members_help     =  _("""
736
        Specify the maximum number of members this project may have,
737
        including the owner. Beyond this number, no new members
738
        may join the project and be granted the project resources.
739
        If you are not certain, it is best to start with a conservative
740
        limit. You can always request a raise when you need it.""")
741

    
742
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
743
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
744

    
745
class ProjectApplicationForm(forms.ModelForm):
746

    
747
    name = forms.CharField(
748
        label     = app_name_label,
749
        help_text = app_name_help,
750
        widget    = app_name_widget,
751
        validators = [app_name_validator])
752

    
753
    homepage = forms.URLField(
754
        label     = app_home_label,
755
        help_text = app_home_help,
756
        widget    = app_home_widget,
757
        required  = False)
758

    
759
    description = forms.CharField(
760
        label     = app_desc_label,
761
        help_text = app_desc_help,
762
        widget    = forms.Textarea,
763
        required  = False)
764

    
765
    comments = forms.CharField(
766
        label     = app_comment_label,
767
        help_text = app_comment_help,
768
        widget    = forms.Textarea,
769
        required  = False)
770

    
771
    start_date = forms.DateTimeField(
772
        label     = app_start_date_label,
773
        help_text = app_start_date_help,
774
        required  = False)
775

    
776
    end_date = forms.DateTimeField(
777
        label     = app_end_date_label,
778
        help_text = app_end_date_help)
779

    
780
    member_join_policy  = forms.TypedChoiceField(
781
        label     = join_policy_label,
782
        help_text = app_member_join_policy_help,
783
        initial   = 2,
784
        coerce    = int,
785
        choices   = join_policies)
786

    
787
    member_leave_policy = forms.TypedChoiceField(
788
        label     = leave_policy_label,
789
        help_text = app_member_leave_policy_help,
790
        coerce    = int,
791
        choices   = leave_policies)
792

    
793
    limit_on_members_number = forms.IntegerField(
794
        label     = max_members_label,
795
        help_text = max_members_help,
796
        min_value = 0,
797
        required  = False)
798

    
799
    class Meta:
800
        model = ProjectApplication
801
        fields = ( 'name', 'homepage', 'description',
802
                    'start_date', 'end_date', 'comments',
803
                    'member_join_policy', 'member_leave_policy',
804
                    'limit_on_members_number')
805

    
806
    def __init__(self, *args, **kwargs):
807
        instance = kwargs.get('instance')
808
        self.precursor_application = instance
809
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
810
        # in case of new application remove closed join policy
811
        if not instance:
812
            policies = PROJECT_MEMBER_JOIN_POLICIES.copy()
813
            policies.pop('3')
814
            self.fields['member_join_policy'].choices = policies.iteritems()
815

    
816
    def clean_start_date(self):
817
        start_date = self.cleaned_data.get('start_date')
818
        if not self.precursor_application:
819
            today = datetime.now()
820
            today = datetime(today.year, today.month, today.day)
821
            if start_date and (start_date - today).days < 0:
822
                raise forms.ValidationError(
823
                _(astakos_messages.INVALID_PROJECT_START_DATE))
824
        return start_date
825

    
826
    def clean_end_date(self):
827
        start_date = self.cleaned_data.get('start_date')
828
        end_date = self.cleaned_data.get('end_date')
829
        today = datetime.now()
830
        today = datetime(today.year, today.month, today.day)
831
        if end_date and (end_date - today).days < 0:
832
            raise forms.ValidationError(
833
                _(astakos_messages.INVALID_PROJECT_END_DATE))
834
        if start_date and (end_date - start_date).days <= 0:
835
            raise forms.ValidationError(
836
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
837
        return end_date
838

    
839
    def clean(self):
840
        userid = self.data.get('user', None)
841
        self.user = None
842
        if userid:
843
            try:
844
                self.user = AstakosUser.objects.get(id=userid)
845
            except AstakosUser.DoesNotExist:
846
                pass
847
        if not self.user:
848
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
849
        super(ProjectApplicationForm, self).clean()
850
        return self.cleaned_data
851

    
852
    @property
853
    def resource_policies(self):
854
        policies = []
855
        append = policies.append
856
        for name, value in self.data.iteritems():
857
            if not value:
858
                continue
859
            uplimit = value
860
            if name.endswith('_uplimit'):
861
                subs = name.split('_uplimit')
862
                prefix, suffix = subs
863
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
864
                resource = Resource.objects.get(service__name=s, name=r)
865

    
866
                # keep only resource limits for selected resource groups
867
                if self.data.get(
868
                    'is_selected_%s' % resource.group, "0"
869
                 ) == "1":
870
                    d = model_to_dict(resource)
871
                    if uplimit:
872
                        d.update(dict(service=s, resource=r, uplimit=uplimit))
873
                    else:
874
                        d.update(dict(service=s, resource=r, uplimit=None))
875
                    append(d)
876

    
877
        ordered_keys = RESOURCES_PRESENTATION_DATA['resources_order']
878
        policies = sorted(policies, key=lambda r:ordered_keys.index(r['str_repr']))
879
        return policies
880

    
881
    def save(self, commit=True):
882
        data = dict(self.cleaned_data)
883
        data['precursor_application'] = self.instance.id
884
        is_new = self.instance.id is None
885
        data['owner'] = self.user if is_new else self.instance.owner
886
        data['resource_policies'] = self.resource_policies
887
        submit_application(data, request_user=self.user)
888

    
889
class ProjectSortForm(forms.Form):
890
    sorting = forms.ChoiceField(
891
        label='Sort by',
892
        choices=(('name', 'Sort by Name'),
893
                 ('issue_date', 'Sort by Issue date'),
894
                 ('start_date', 'Sort by Start Date'),
895
                 ('end_date', 'Sort by End Date'),
896
#                  ('approved_members_num', 'Sort by Participants'),
897
                 ('state', 'Sort by Status'),
898
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
899
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
900
                 ('-name', 'Sort by Name'),
901
                 ('-issue_date', 'Sort by Issue date'),
902
                 ('-start_date', 'Sort by Start Date'),
903
                 ('-end_date', 'Sort by End Date'),
904
#                  ('-approved_members_num', 'Sort by Participants'),
905
                 ('-state', 'Sort by Status'),
906
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
907
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
908
        ),
909
        required=True
910
    )
911

    
912
class AddProjectMembersForm(forms.Form):
913
    q = forms.CharField(
914
        max_length=800, widget=forms.Textarea, label=_('Add members'),
915
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
916

    
917
    def __init__(self, *args, **kwargs):
918
        chain_id = kwargs.pop('chain_id', None)
919
        if chain_id:
920
            self.project = Project.objects.get(id=chain_id)
921
        self.request_user = kwargs.pop('request_user', None)
922
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
923

    
924
    def clean(self):
925
        try:
926
            accept_membership_checks(self.project, self.request_user)
927
        except PermissionDenied, e:
928
            raise forms.ValidationError(e)
929

    
930
        q = self.cleaned_data.get('q') or ''
931
        users = q.split(',')
932
        users = list(u.strip() for u in users if u)
933
        db_entries = AstakosUser.objects.verified().filter(email__in=users)
934
        unknown = list(set(users) - set(u.email for u in db_entries))
935
        if unknown:
936
            raise forms.ValidationError(
937
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
938
        self.valid_users = db_entries
939
        return self.cleaned_data
940

    
941
    def get_valid_users(self):
942
        """Should be called after form cleaning"""
943
        try:
944
            return self.valid_users
945
        except:
946
            return ()
947

    
948
class ProjectMembersSortForm(forms.Form):
949
    sorting = forms.ChoiceField(
950
        label='Sort by',
951
        choices=(('person__email', 'User Id'),
952
                 ('person__first_name', 'Name'),
953
                 ('acceptance_date', 'Acceptance date')
954
        ),
955
        required=True
956
    )
957

    
958

    
959
class ProjectSearchForm(forms.Form):
960
    q = forms.CharField(max_length=200, label='Search project', required=False)
961

    
962

    
963
class ExtendedProfileForm(ProfileForm):
964
    """
965
    Profile form that combines `email change` and `password change` user
966
    actions by propagating submited data to internal EmailChangeForm
967
    and ExtendedPasswordChangeForm objects.
968
    """
969

    
970
    password_change_form = None
971
    email_change_form = None
972

    
973
    password_change = False
974
    email_change = False
975

    
976
    extra_forms_fields = {
977
        'email': ['new_email_address'],
978
        'password': ['old_password', 'new_password1', 'new_password2']
979
    }
980

    
981
    fields = ('email')
982
    change_password = forms.BooleanField(initial=False, required=False)
983
    change_email = forms.BooleanField(initial=False, required=False)
984

    
985
    email_changed = False
986
    password_changed = False
987

    
988
    def __init__(self, *args, **kwargs):
989
        session_key = kwargs.get('session_key', None)
990
        self.fields_list = [
991
                'email',
992
                'new_email_address',
993
                'first_name',
994
                'last_name',
995
                'auth_token',
996
                'auth_token_expires',
997
                'old_password',
998
                'new_password1',
999
                'new_password2',
1000
                'change_email',
1001
                'change_password',
1002
                'uuid'
1003
        ]
1004

    
1005
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
1006
        self.session_key = session_key
1007
        if self.instance.can_change_password():
1008
            self.password_change = True
1009
        else:
1010
            self.fields_list.remove('old_password')
1011
            self.fields_list.remove('new_password1')
1012
            self.fields_list.remove('new_password2')
1013
            self.fields_list.remove('change_password')
1014
            del self.fields['change_password']
1015

    
1016
        if EMAILCHANGE_ENABLED and self.instance.can_change_email():
1017
            self.email_change = True
1018
        else:
1019
            self.fields_list.remove('new_email_address')
1020
            self.fields_list.remove('change_email')
1021
            del self.fields['change_email']
1022

    
1023
        self._init_extra_forms()
1024
        self.save_extra_forms = []
1025
        self.success_messages = []
1026
        self.fields.keyOrder = self.fields_list
1027

    
1028

    
1029
    def _init_extra_form_fields(self):
1030
        if self.email_change:
1031
            self.fields.update(self.email_change_form.fields)
1032
            self.fields['new_email_address'].required = False
1033
            self.fields['email'].help_text = _('Change the email associated with '
1034
                                               'your account. This email will '
1035
                                               'remain active until you verify '
1036
                                               'your new one.')
1037

    
1038
        if self.password_change:
1039
            self.fields.update(self.password_change_form.fields)
1040
            self.fields['old_password'].required = False
1041
            self.fields['old_password'].label = _('Password')
1042
            self.fields['old_password'].help_text = _('Change your password.')
1043
            self.fields['old_password'].initial = 'password'
1044
            self.fields['new_password1'].required = False
1045
            self.fields['new_password2'].required = False
1046

    
1047
    def _update_extra_form_errors(self):
1048
        if self.cleaned_data.get('change_password'):
1049
            self.errors.update(self.password_change_form.errors)
1050
        if self.cleaned_data.get('change_email'):
1051
            self.errors.update(self.email_change_form.errors)
1052

    
1053
    def _init_extra_forms(self):
1054
        self.email_change_form = EmailChangeForm(self.data)
1055
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1056
                                   data=self.data, session_key=self.session_key)
1057
        self._init_extra_form_fields()
1058

    
1059
    def is_valid(self):
1060
        password, email = True, True
1061
        profile = super(ExtendedProfileForm, self).is_valid()
1062
        if profile and self.cleaned_data.get('change_password', None):
1063

    
1064
            password = self.password_change_form.is_valid()
1065
            self.save_extra_forms.append('password')
1066
        if profile and self.cleaned_data.get('change_email'):
1067
            self.fields['new_email_address'].required = True
1068
            email = self.email_change_form.is_valid()
1069
            self.save_extra_forms.append('email')
1070

    
1071
        if not password or not email:
1072
            self._update_extra_form_errors()
1073

    
1074
        return all([profile, password, email])
1075

    
1076
    def save(self, request, *args, **kwargs):
1077
        if 'email' in self.save_extra_forms:
1078
            self.email_change_form.save(request, *args, **kwargs)
1079
            self.email_changed = True
1080
        if 'password' in self.save_extra_forms:
1081
            self.password_change_form.save(*args, **kwargs)
1082
            self.password_changed = True
1083
        return super(ExtendedProfileForm, self).save(*args, **kwargs)
1084