Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 1177e91b

History | View | Annotate | Download (38.4 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
from astakos.im.widgets import DummyWidget, RecaptchaWidget
69
from astakos.im.functions import (
70
    send_change_email, submit_application, accept_membership_checks)
71

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

    
76
import astakos.im.messages as astakos_messages
77

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

    
83
logger = logging.getLogger(__name__)
84

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

    
89
class StoreUserMixin(object):
90

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

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

    
106

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
203

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

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

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

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

    
231

    
232
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
233
    id = forms.CharField(
234
        widget=forms.HiddenInput(),
235
        label='',
236
        required=False
237
    )
238
    third_party_identifier = forms.CharField(
239
        widget=forms.HiddenInput(),
240
        label=''
241
    )
242
    email = forms.EmailField(
243
        label='Contact email',
244
        help_text = 'We will use this email to contact you'
245
    )
246

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

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

    
260
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
261

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

    
270
    def clean_email(self):
271
        email = self.cleaned_data['email']
272
        if not email:
273
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
274
        if reserved_verified_email(email):
275
            provider = auth_providers.get_provider(self.request.REQUEST.get('provider', 'local'))
276
            extra_message = _(astakos_messages.EXISTING_EMAIL_THIRD_PARTY_NOTIFICATION) % \
277
                    (provider.get_title_display, reverse('edit_profile'))
278

    
279
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED) + ' ' + \
280
                                        extra_message)
281
        return email
282

    
283
    def clean_has_signed_terms(self):
284
        has_signed_terms = self.cleaned_data['has_signed_terms']
285
        if not has_signed_terms:
286
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
287
        return has_signed_terms
288

    
289
    def post_store_user(self, user, request):
290
        pending = PendingThirdPartyUser.objects.get(
291
                                token=request.POST.get('third_party_token'),
292
                                third_party_identifier= \
293
                            self.cleaned_data.get('third_party_identifier'))
294
        return user.add_pending_auth_provider(pending)
295

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

    
305

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

    
317
        #set readonly form fields
318
        ro = ('email',)
319
        for f in ro:
320
            self.fields[f].widget.attrs['readonly'] = True
321

    
322
    def save(self, commit=True):
323
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
324
        user.set_invitation_level()
325
        user.email_verified = True
326
        if commit:
327
            user.save()
328
        return user
329

    
330

    
331
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
332
    additional_email = forms.CharField(
333
        widget=forms.HiddenInput(), label='', required=False)
334

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

    
343

    
344
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
345
                                        InvitedThirdPartyUserCreationForm):
346
    pass
347

    
348

    
349
class LoginForm(AuthenticationForm):
350
    username = forms.EmailField(label=_("Email"))
351
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
352
    recaptcha_response_field = forms.CharField(
353
        widget=RecaptchaWidget, label='')
354

    
355
    def __init__(self, *args, **kwargs):
356
        was_limited = kwargs.get('was_limited', False)
357
        request = kwargs.get('request', None)
358
        if request:
359
            self.ip = request.META.get('REMOTE_ADDR',
360
                                       request.META.get('HTTP_X_REAL_IP', None))
361

    
362
        t = ('request', 'was_limited')
363
        for elem in t:
364
            if elem in kwargs.keys():
365
                kwargs.pop(elem)
366
        super(LoginForm, self).__init__(*args, **kwargs)
367

    
368
        self.fields.keyOrder = ['username', 'password']
369
        if was_limited and RECAPTCHA_ENABLED:
370
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
371
                                         'recaptcha_response_field', ])
372

    
373
    def clean_username(self):
374
        return self.cleaned_data['username'].lower()
375

    
376
    def clean_recaptcha_response_field(self):
377
        if 'recaptcha_challenge_field' in self.cleaned_data:
378
            self.validate_captcha()
379
        return self.cleaned_data['recaptcha_response_field']
380

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

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

    
393
    def clean(self):
394
        """
395
        Override default behavior in order to check user's activation later
396
        """
397
        username = self.cleaned_data.get('username')
398

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

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

    
421

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

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

    
433
    class Meta:
434
        model = AstakosUser
435
        fields = ('email', 'first_name', 'last_name', 'auth_token',
436
                  'auth_token_expires')
437

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

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

    
459

    
460

    
461
class FeedbackForm(forms.Form):
462
    """
463
    Form for writing feedback.
464
    """
465
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
466
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
467
                                    required=False)
468

    
469

    
470
class SendInvitationForm(forms.Form):
471
    """
472
    Form for sending an invitations
473
    """
474

    
475
    email = forms.EmailField(required=True, label='Email address')
476
    first_name = forms.EmailField(label='First name')
477
    last_name = forms.EmailField(label='Last name')
478

    
479

    
480
class ExtendedPasswordResetForm(PasswordResetForm):
481
    """
482
    Extends PasswordResetForm by overriding
483

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

    
492
            if not user.is_active:
493
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
494

    
495
            if not user.has_usable_password():
496
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
497

    
498
            if not user.can_change_password():
499
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
500
        except AstakosUser.DoesNotExist, e:
501
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
502
        return email
503

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

    
529

    
530
class EmailChangeForm(forms.ModelForm):
531

    
532
    class Meta:
533
        model = EmailChange
534
        fields = ('new_email_address',)
535

    
536
    def clean_new_email_address(self):
537
        addr = self.cleaned_data['new_email_address']
538
        if reserved_verified_email(addr):
539
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
540
        return addr
541

    
542
    def save(self, request, email_template_name='registration/email_change_email.txt', commit=True):
543
        ec = super(EmailChangeForm, self).save(commit=False)
544
        ec.user = request.user
545
        # delete pending email changes
546
        request.user.emailchanges.all().delete()
547

    
548
        activation_key = hashlib.sha1(
549
            str(random()) + smart_str(ec.new_email_address))
550
        ec.activation_key = activation_key.hexdigest()
551
        if commit:
552
            ec.save()
553
        send_change_email(ec, request, email_template_name=email_template_name)
554

    
555

    
556
class SignApprovalTermsForm(forms.ModelForm):
557

    
558
    class Meta:
559
        model = AstakosUser
560
        fields = ("has_signed_terms",)
561

    
562
    def __init__(self, *args, **kwargs):
563
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
564

    
565
    def clean_has_signed_terms(self):
566
        has_signed_terms = self.cleaned_data['has_signed_terms']
567
        if not has_signed_terms:
568
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
569
        return has_signed_terms
570

    
571

    
572
class InvitationForm(forms.ModelForm):
573

    
574
    username = forms.EmailField(label=_("Email"))
575

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

    
579
    class Meta:
580
        model = Invitation
581
        fields = ('username', 'realname')
582

    
583
    def clean_username(self):
584
        username = self.cleaned_data['username']
585
        try:
586
            Invitation.objects.get(username=username)
587
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
588
        except Invitation.DoesNotExist:
589
            pass
590
        return username
591

    
592

    
593
class ExtendedPasswordChangeForm(PasswordChangeForm):
594
    """
595
    Extends PasswordChangeForm by enabling user
596
    to optionally renew also the token.
597
    """
598
    if not NEWPASSWD_INVALIDATE_TOKEN:
599
        renew = forms.BooleanField(label='Renew token', required=False,
600
                                   initial=True,
601
                                   help_text='Unsetting this may result in security risk.')
602

    
603
    def __init__(self, user, *args, **kwargs):
604
        self.session_key = kwargs.pop('session_key', None)
605
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
606

    
607
    def save(self, commit=True):
608
        try:
609
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
610
                self.user.renew_token()
611
            self.user.flush_sessions(current_key=self.session_key)
612
        except AttributeError:
613
            # if user model does has not such methods
614
            pass
615
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
616

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

    
629
    def __init__(self, user, *args, **kwargs):
630
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
631

    
632
    @transaction.commit_on_success()
633
    def save(self, commit=True):
634
        try:
635
            self.user = AstakosUser.objects.get(id=self.user.id)
636
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
637
                self.user.renew_token()
638
            #self.user.flush_sessions()
639
            if not self.user.has_auth_provider('local'):
640
                self.user.add_auth_provider('local', auth_backend='astakos')
641

    
642
        except BaseException, e:
643
            logger.exception(e)
644
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
645

    
646

    
647

    
648

    
649
app_name_label       =  "Project name"
650
app_name_placeholder = _("myproject.mylab.ntua.gr")
651
app_name_validator   =  validators.RegexValidator(
652
                            DOMAIN_VALUE_REGEX,
653
                            _(astakos_messages.DOMAIN_VALUE_ERR),
654
                            'invalid')
655
app_name_help        =  _("""
656
        The Project's name should be in a domain format.
657
        The domain shouldn't neccessarily exist in the real
658
        world but is helpful to imply a structure.
659
        e.g.: myproject.mylab.ntua.gr or
660
        myservice.myteam.myorganization""")
661
app_name_widget      =  forms.TextInput(
662
                            attrs={'placeholder': app_name_placeholder})
663

    
664

    
665
app_home_label       =  "Homepage URL"
666
app_home_placeholder =  'myinstitution.org/myproject/'
667
app_home_help        =  _("""
668
        URL pointing at your project's site.
669
        e.g.: myinstitution.org/myproject/.
670
        Leave blank if there is no website.""")
671
app_home_widget      =  forms.TextInput(
672
                            attrs={'placeholder': app_home_placeholder})
673

    
674
app_desc_label       =  _("Description")
675
app_desc_help        =  _("""
676
        Please provide a short but descriptive abstract of your Project,
677
        so that anyone searching can quickly understand
678
        what this Project is about.""")
679

    
680
app_comment_label    =  _("Comments for review (private)")
681
app_comment_help     =  _("""
682
        Write down any comments you may have for the reviewer
683
        of this application (e.g. background and rationale to
684
        support your request).
685
        The comments are strictly for the review process
686
        and will not be published.""")
687

    
688
app_start_date_label =  _("Start date")
689
app_start_date_help  =  _("""
690
        Provide a date when your need your project to be created,
691
        and members to be able to join and get resources.
692
        This date is only a hint to help prioritize reviews.""")
693

    
694
app_end_date_label   =  _("Termination date")
695
app_end_date_help    =  _("""
696
        At this date, the project will be automatically terminated
697
        and its resource grants revoked from all members.
698
        Unless you know otherwise,
699
        it is best to start with a conservative estimation.
700
        You can always re-apply for an extension, if you need.""")
701

    
702
join_policy_label    =  _("Joining policy")
703
app_member_join_policy_help    =  _("""
704
        Text fo member_join_policy.""")
705
leave_policy_label   =  _("Leaving policy")
706
app_member_leave_policy_help    =  _("""
707
        Text fo member_leave_policy.""")
708

    
709
max_members_label    =  _("Maximum member count")
710
max_members_help     =  _("""
711
        Specify the maximum number of members this project may have,
712
        including the owner. Beyond this number, no new members
713
        may join the project and be granted the project resources.
714
        Unless you certainly for otherwise,
715
        it is best to start with a conservative limit.
716
        You can always request a raise when you need it.""")
717

    
718
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
719
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
720

    
721
class ProjectApplicationForm(forms.ModelForm):
722

    
723
    name = forms.CharField(
724
        label     = app_name_label,
725
        help_text = app_name_help,
726
        widget    = app_name_widget,
727
        validators = [app_name_validator])
728

    
729
    homepage = forms.URLField(
730
        label     = app_home_label,
731
        help_text = app_home_help,
732
        widget    = app_home_widget,
733
        required  = False)
734

    
735
    description = forms.CharField(
736
        label     = app_desc_label,
737
        help_text = app_desc_help,
738
        widget    = forms.Textarea,
739
        required  = False)
740

    
741
    comments = forms.CharField(
742
        label     = app_comment_label,
743
        help_text = app_comment_help,
744
        widget    = forms.Textarea,
745
        required  = False)
746

    
747
    start_date = forms.DateTimeField(
748
        label     = app_start_date_label,
749
        help_text = app_start_date_help,
750
        required  = False)
751

    
752
    end_date = forms.DateTimeField(
753
        label     = app_end_date_label,
754
        help_text = app_end_date_help)
755

    
756
    member_join_policy  = forms.TypedChoiceField(
757
        label     = join_policy_label,
758
        help_text = app_member_join_policy_help,
759
        initial   = 2,
760
        coerce    = int,
761
        choices   = join_policies)
762

    
763
    member_leave_policy = forms.TypedChoiceField(
764
        label     = leave_policy_label,
765
        help_text = app_member_leave_policy_help,
766
        coerce    = int,
767
        choices   = leave_policies)
768

    
769
    limit_on_members_number = forms.IntegerField(
770
        label     = max_members_label,
771
        help_text = max_members_help,
772
        required  = False)
773

    
774
    class Meta:
775
        model = ProjectApplication
776
        fields = ( 'name', 'homepage', 'description',
777
                    'start_date', 'end_date', 'comments',
778
                    'member_join_policy', 'member_leave_policy',
779
                    'limit_on_members_number')
780

    
781
    def __init__(self, *args, **kwargs):
782
        instance = kwargs.get('instance')
783
        self.precursor_application = instance
784
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
785
        # in case of new application remove closed join policy
786
        if not instance:
787
            policies = PROJECT_MEMBER_JOIN_POLICIES.copy()
788
            policies.pop('3')
789
            self.fields['member_join_policy'].choices = policies.iteritems()
790

    
791
    def clean_start_date(self):
792
        start_date = self.cleaned_data.get('start_date')
793
        if not self.precursor_application:
794
            today = datetime.now()
795
            today = datetime(today.year, today.month, today.day)
796
            if start_date and (start_date - today).days < 0:
797
                raise forms.ValidationError(
798
                _(astakos_messages.INVALID_PROJECT_START_DATE))
799
        return start_date
800

    
801
    def clean_end_date(self):
802
        start_date = self.cleaned_data.get('start_date')
803
        end_date = self.cleaned_data.get('end_date')
804
        today = datetime.now()
805
        today = datetime(today.year, today.month, today.day)
806
        if end_date and (end_date - today).days < 0:
807
            raise forms.ValidationError(
808
                _(astakos_messages.INVALID_PROJECT_END_DATE))
809
        if start_date and (end_date - start_date).days <= 0:
810
            raise forms.ValidationError(
811
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
812
        return end_date
813

    
814
    def clean(self):
815
        userid = self.data.get('user', None)
816
        self.user = None
817
        if userid:
818
            try:
819
                self.user = AstakosUser.objects.get(id=userid)
820
            except AstakosUser.DoesNotExist:
821
                pass
822
        if not self.user:
823
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
824
        super(ProjectApplicationForm, self).clean()
825
        return self.cleaned_data
826

    
827
    @property
828
    def resource_policies(self):
829
        policies = []
830
        append = policies.append
831
        for name, value in self.data.iteritems():
832
            if not value:
833
                continue
834
            uplimit = value
835
            if name.endswith('_uplimit'):
836
                subs = name.split('_uplimit')
837
                prefix, suffix = subs
838
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
839
                resource = Resource.objects.get(service__name=s, name=r)
840

    
841
                # keep only resource limits for selected resource groups
842
                if self.data.get(
843
                    'is_selected_%s' % resource.group, "0"
844
                 ) == "1":
845
                    d = model_to_dict(resource)
846
                    if uplimit:
847
                        d.update(dict(service=s, resource=r, uplimit=uplimit))
848
                    else:
849
                        d.update(dict(service=s, resource=r, uplimit=None))
850
                    append(d)
851

    
852
        return policies
853

    
854
    def save(self, commit=True):
855
        data = dict(self.cleaned_data)
856
        data['precursor_application'] = self.instance.id
857
        data['owner'] = self.user
858
        data['resource_policies'] = self.resource_policies
859
        submit_application(data, request_user=self.user)
860

    
861
class ProjectSortForm(forms.Form):
862
    sorting = forms.ChoiceField(
863
        label='Sort by',
864
        choices=(('name', 'Sort by Name'),
865
                 ('issue_date', 'Sort by Issue date'),
866
                 ('start_date', 'Sort by Start Date'),
867
                 ('end_date', 'Sort by End Date'),
868
#                  ('approved_members_num', 'Sort by Participants'),
869
                 ('state', 'Sort by Status'),
870
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
871
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
872
                 ('-name', 'Sort by Name'),
873
                 ('-issue_date', 'Sort by Issue date'),
874
                 ('-start_date', 'Sort by Start Date'),
875
                 ('-end_date', 'Sort by End Date'),
876
#                  ('-approved_members_num', 'Sort by Participants'),
877
                 ('-state', 'Sort by Status'),
878
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
879
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
880
        ),
881
        required=True
882
    )
883

    
884
class AddProjectMembersForm(forms.Form):
885
    q = forms.CharField(
886
        max_length=800, widget=forms.Textarea, label=_('Add members'),
887
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
888

    
889
    def __init__(self, *args, **kwargs):
890
        chain_id = kwargs.pop('chain_id', None)
891
        if chain_id:
892
            self.project = Project.objects.get(id=chain_id)
893
        self.request_user = kwargs.pop('request_user', None)
894
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
895

    
896
    def clean(self):
897
        try:
898
            accept_membership_checks(self.project, self.request_user)
899
        except PermissionDenied, e:
900
            raise forms.ValidationError(e)
901

    
902
        q = self.cleaned_data.get('q') or ''
903
        users = q.split(',')
904
        users = list(u.strip() for u in users if u)
905
        db_entries = AstakosUser.objects.filter(email__in=users)
906
        unknown = list(set(users) - set(u.email for u in db_entries))
907
        if unknown:
908
            raise forms.ValidationError(
909
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
910
        self.valid_users = db_entries
911
        return self.cleaned_data
912

    
913
    def get_valid_users(self):
914
        """Should be called after form cleaning"""
915
        try:
916
            return self.valid_users
917
        except:
918
            return ()
919

    
920
class ProjectMembersSortForm(forms.Form):
921
    sorting = forms.ChoiceField(
922
        label='Sort by',
923
        choices=(('person__email', 'User Id'),
924
                 ('person__first_name', 'Name'),
925
                 ('acceptance_date', 'Acceptance date')
926
        ),
927
        required=True
928
    )
929

    
930

    
931
class ProjectSearchForm(forms.Form):
932
    q = forms.CharField(max_length=200, label='Search project', required=False)
933

    
934

    
935
class ExtendedProfileForm(ProfileForm):
936
    """
937
    Profile form that combines `email change` and `password change` user
938
    actions by propagating submited data to internal EmailChangeForm
939
    and ExtendedPasswordChangeForm objects.
940
    """
941

    
942
    password_change_form = None
943
    email_change_form = None
944

    
945
    password_change = False
946
    email_change = False
947

    
948
    extra_forms_fields = {
949
        'email': ['new_email_address'],
950
        'password': ['old_password', 'new_password1', 'new_password2']
951
    }
952

    
953
    fields = ('email')
954
    change_password = forms.BooleanField(initial=False, required=False)
955
    change_email = forms.BooleanField(initial=False, required=False)
956

    
957
    email_changed = False
958
    password_changed = False
959

    
960
    def __init__(self, *args, **kwargs):
961
        session_key = kwargs.get('session_key', None)
962
        self.fields_list = [
963
                'email',
964
                'new_email_address',
965
                'first_name',
966
                'last_name',
967
                'auth_token',
968
                'auth_token_expires',
969
                'old_password',
970
                'new_password1',
971
                'new_password2',
972
                'change_email',
973
                'change_password',
974
        ]
975

    
976
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
977
        self.session_key = session_key
978
        if self.instance.can_change_password():
979
            self.password_change = True
980
        else:
981
            self.fields_list.remove('old_password')
982
            self.fields_list.remove('new_password1')
983
            self.fields_list.remove('new_password2')
984
            self.fields_list.remove('change_password')
985
            del self.fields['change_password']
986

    
987

    
988
        if EMAILCHANGE_ENABLED and self.instance.can_change_email():
989
            self.email_change = True
990
        else:
991
            self.fields_list.remove('new_email_address')
992
            self.fields_list.remove('change_email')
993
            del self.fields['change_email']
994

    
995
        self._init_extra_forms()
996
        self.save_extra_forms = []
997
        self.success_messages = []
998
        self.fields.keyOrder = self.fields_list
999

    
1000

    
1001
    def _init_extra_form_fields(self):
1002
        
1003

    
1004
        if self.email_change:
1005
            self.fields.update(self.email_change_form.fields)
1006
            self.fields['new_email_address'].required = False
1007

    
1008
        if self.password_change:
1009
            self.fields.update(self.password_change_form.fields)
1010
            self.fields['old_password'].required = False
1011
            self.fields['old_password'].label = _('Password')
1012
            self.fields['old_password'].initial = 'skata'
1013
            self.fields['new_password1'].required = False
1014
            self.fields['new_password2'].required = False
1015

    
1016
    def _update_extra_form_errors(self):
1017
        if self.cleaned_data.get('change_password'):
1018
            self.errors.update(self.password_change_form.errors)
1019
        if self.cleaned_data.get('change_email'):
1020
            self.errors.update(self.email_change_form.errors)
1021

    
1022
    def _init_extra_forms(self):
1023
        self.email_change_form = EmailChangeForm(self.data)
1024
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1025
                                   data=self.data, session_key=self.session_key)
1026
        self._init_extra_form_fields()
1027

    
1028
    def is_valid(self):
1029
        password, email = True, True
1030
        profile = super(ExtendedProfileForm, self).is_valid()
1031
        if profile and self.cleaned_data.get('change_password', None):
1032
            password = self.password_change_form.is_valid()
1033
            self.save_extra_forms.append('password')
1034
        if profile and self.cleaned_data.get('change_email'):
1035
            email = self.email_change_form.is_valid()
1036
            self.save_extra_forms.append('email')
1037

    
1038
        if not password or not email:
1039
            self._update_extra_form_errors()
1040

    
1041
        return all([profile, password, email])
1042

    
1043
    def save(self, request, *args, **kwargs):
1044
        if 'email' in self.save_extra_forms:
1045
            self.email_change_form.save(request, *args, **kwargs)
1046
            self.email_changed = True
1047
        if 'password' in self.save_extra_forms:
1048
            self.password_change_form.save(*args, **kwargs)
1049
            self.password_changed = True
1050
        return super(ExtendedProfileForm, self).save(*args, **kwargs)
1051