Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 649f2d36

History | View | Annotate | Download (37.7 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
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, do_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

    
243
    class Meta:
244
        model = AstakosUser
245
        fields = ['id', 'email', 'third_party_identifier',
246
                  'first_name', 'last_name', 'has_signed_terms']
247

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

    
256
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
257

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

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

    
275
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED) + ' ' + \
276
                                        extra_message)
277
        return email
278

    
279
    def clean_has_signed_terms(self):
280
        has_signed_terms = self.cleaned_data['has_signed_terms']
281
        if not has_signed_terms:
282
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
283
        return has_signed_terms
284

    
285
    def post_store_user(self, user, request):
286
        pending = PendingThirdPartyUser.objects.get(
287
                                token=request.POST.get('third_party_token'),
288
                                third_party_identifier= \
289
                            self.cleaned_data.get('third_party_identifier'))
290
        return user.add_pending_auth_provider(pending)
291

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

    
301

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

    
313
        #set readonly form fields
314
        ro = ('email',)
315
        for f in ro:
316
            self.fields[f].widget.attrs['readonly'] = True
317

    
318
    def save(self, commit=True):
319
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
320
        user.set_invitation_level()
321
        user.email_verified = True
322
        if commit:
323
            user.save()
324
        return user
325

    
326

    
327
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
328
    additional_email = forms.CharField(
329
        widget=forms.HiddenInput(), label='', required=False)
330

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

    
339

    
340
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
341
                                        InvitedThirdPartyUserCreationForm):
342
    pass
343

    
344

    
345
class LoginForm(AuthenticationForm):
346
    username = forms.EmailField(label=_("Email"))
347
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
348
    recaptcha_response_field = forms.CharField(
349
        widget=RecaptchaWidget, label='')
350

    
351
    def __init__(self, *args, **kwargs):
352
        was_limited = kwargs.get('was_limited', False)
353
        request = kwargs.get('request', None)
354
        if request:
355
            self.ip = request.META.get('REMOTE_ADDR',
356
                                       request.META.get('HTTP_X_REAL_IP', None))
357

    
358
        t = ('request', 'was_limited')
359
        for elem in t:
360
            if elem in kwargs.keys():
361
                kwargs.pop(elem)
362
        super(LoginForm, self).__init__(*args, **kwargs)
363

    
364
        self.fields.keyOrder = ['username', 'password']
365
        if was_limited and RECAPTCHA_ENABLED:
366
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
367
                                         'recaptcha_response_field', ])
368

    
369
    def clean_username(self):
370
        return self.cleaned_data['username'].lower()
371

    
372
    def clean_recaptcha_response_field(self):
373
        if 'recaptcha_challenge_field' in self.cleaned_data:
374
            self.validate_captcha()
375
        return self.cleaned_data['recaptcha_response_field']
376

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

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

    
389
    def clean(self):
390
        """
391
        Override default behavior in order to check user's activation later
392
        """
393
        username = self.cleaned_data.get('username')
394

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

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

    
417

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

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

    
429
    class Meta:
430
        model = AstakosUser
431
        fields = ('email', 'first_name', 'last_name', 'auth_token',
432
                  'auth_token_expires')
433

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

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

    
455

    
456

    
457
class FeedbackForm(forms.Form):
458
    """
459
    Form for writing feedback.
460
    """
461
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
462
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
463
                                    required=False)
464

    
465

    
466
class SendInvitationForm(forms.Form):
467
    """
468
    Form for sending an invitations
469
    """
470

    
471
    email = forms.EmailField(required=True, label='Email address')
472
    first_name = forms.EmailField(label='First name')
473
    last_name = forms.EmailField(label='Last name')
474

    
475

    
476
class ExtendedPasswordResetForm(PasswordResetForm):
477
    """
478
    Extends PasswordResetForm by overriding
479

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

    
488
            if not user.is_active:
489
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
490

    
491
            if not user.has_usable_password():
492
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
493

    
494
            if not user.can_change_password():
495
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
496
        except AstakosUser.DoesNotExist, e:
497
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
498
        return email
499

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

    
522

    
523
class EmailChangeForm(forms.ModelForm):
524

    
525
    class Meta:
526
        model = EmailChange
527
        fields = ('new_email_address',)
528

    
529
    def clean_new_email_address(self):
530
        addr = self.cleaned_data['new_email_address']
531
        if reserved_verified_email(addr):
532
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
533
        return addr
534

    
535
    def save(self, request, email_template_name='registration/email_change_email.txt', commit=True):
536
        ec = super(EmailChangeForm, self).save(commit=False)
537
        ec.user = request.user
538
        # delete pending email changes
539
        request.user.emailchanges.all().delete()
540

    
541
        activation_key = hashlib.sha1(
542
            str(random()) + smart_str(ec.new_email_address))
543
        ec.activation_key = activation_key.hexdigest()
544
        if commit:
545
            ec.save()
546
        send_change_email(ec, request, email_template_name=email_template_name)
547

    
548

    
549
class SignApprovalTermsForm(forms.ModelForm):
550

    
551
    class Meta:
552
        model = AstakosUser
553
        fields = ("has_signed_terms",)
554

    
555
    def __init__(self, *args, **kwargs):
556
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
557

    
558
    def clean_has_signed_terms(self):
559
        has_signed_terms = self.cleaned_data['has_signed_terms']
560
        if not has_signed_terms:
561
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
562
        return has_signed_terms
563

    
564

    
565
class InvitationForm(forms.ModelForm):
566

    
567
    username = forms.EmailField(label=_("Email"))
568

    
569
    def __init__(self, *args, **kwargs):
570
        super(InvitationForm, self).__init__(*args, **kwargs)
571

    
572
    class Meta:
573
        model = Invitation
574
        fields = ('username', 'realname')
575

    
576
    def clean_username(self):
577
        username = self.cleaned_data['username']
578
        try:
579
            Invitation.objects.get(username=username)
580
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
581
        except Invitation.DoesNotExist:
582
            pass
583
        return username
584

    
585

    
586
class ExtendedPasswordChangeForm(PasswordChangeForm):
587
    """
588
    Extends PasswordChangeForm by enabling user
589
    to optionally renew also the token.
590
    """
591
    if not NEWPASSWD_INVALIDATE_TOKEN:
592
        renew = forms.BooleanField(label='Renew token', required=False,
593
                                   initial=True,
594
                                   help_text='Unsetting this may result in security risk.')
595

    
596
    def __init__(self, user, *args, **kwargs):
597
        self.session_key = kwargs.pop('session_key', None)
598
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
599

    
600
    def save(self, commit=True):
601
        try:
602
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
603
                self.user.renew_token()
604
            self.user.flush_sessions(current_key=self.session_key)
605
        except AttributeError:
606
            # if user model does has not such methods
607
            pass
608
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
609

    
610
class ExtendedSetPasswordForm(SetPasswordForm):
611
    """
612
    Extends SetPasswordForm by enabling user
613
    to optionally renew also the token.
614
    """
615
    if not NEWPASSWD_INVALIDATE_TOKEN:
616
        renew = forms.BooleanField(
617
            label='Renew token',
618
            required=False,
619
            initial=True,
620
            help_text='Unsetting this may result in security risk.')
621

    
622
    def __init__(self, user, *args, **kwargs):
623
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
624

    
625
    @transaction.commit_on_success()
626
    def save(self, commit=True):
627
        try:
628
            self.user = AstakosUser.objects.get(id=self.user.id)
629
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
630
                self.user.renew_token()
631
            #self.user.flush_sessions()
632
            if not self.user.has_auth_provider('local'):
633
                self.user.add_auth_provider('local', auth_backend='astakos')
634

    
635
        except BaseException, e:
636
            logger.exception(e)
637
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
638

    
639

    
640

    
641

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

    
657

    
658
app_home_label       =  "Homepage URL"
659
app_home_placeholder =  'myinstitution.org/myproject/'
660
app_home_help        =  _("""
661
        URL pointing at your project's site.
662
        e.g.: myinstitution.org/myproject/.
663
        Leave blank if there is no website.""")
664
app_home_widget      =  forms.TextInput(
665
                            attrs={'placeholder': app_home_placeholder})
666

    
667
app_desc_label       =  _("Description")
668
app_desc_help        =  _("""
669
        Please provide a short but descriptive abstract of your Project,
670
        so that anyone searching can quickly understand
671
        what this Project is about.""")
672

    
673
app_comment_label    =  _("Comments for review (private)")
674
app_comment_help     =  _("""
675
        Write down any comments you may have for the reviewer
676
        of this application (e.g. background and rationale to
677
        support your request).
678
        The comments are strictly for the review process
679
        and will not be published.""")
680

    
681
app_start_date_label =  _("Start date")
682
app_start_date_help  =  _("""
683
        Provide a date when your need your project to be created,
684
        and members to be able to join and get resources.
685
        This date is only a hint to help prioritize reviews.""")
686

    
687
app_end_date_label   =  _("Termination date")
688
app_end_date_help    =  _("""
689
        At this date, the project will be automatically terminated
690
        and its resource grants revoked from all members.
691
        Unless you know otherwise,
692
        it is best to start with a conservative estimation.
693
        You can always re-apply for an extension, if you need.""")
694

    
695
join_policy_label    =  _("Joining policy")
696
leave_policy_label   =  _("Leaving policy")
697

    
698
max_members_label    =  _("Maximum member count")
699
max_members_help     =  _("""
700
        Specify the maximum number of members this project may have,
701
        including the owner. Beyond this number, no new members
702
        may join the project and be granted the project resources.
703
        Unless you certainly for otherwise,
704
        it is best to start with a conservative limit.
705
        You can always request a raise when you need it.""")
706

    
707
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
708
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
709

    
710
class ProjectApplicationForm(forms.ModelForm):
711

    
712
    name = forms.CharField(
713
        label     = app_name_label,
714
        help_text = app_name_help,
715
        widget    = app_name_widget,
716
        validators = [app_name_validator])
717

    
718
    homepage = forms.URLField(
719
        label     = app_home_label,
720
        help_text = app_home_help,
721
        widget    = app_home_widget,
722
        required  = False)
723

    
724
    description = forms.CharField(
725
        label     = app_desc_label,
726
        help_text = app_desc_help,
727
        widget    = forms.Textarea,
728
        required  = False)
729

    
730
    comments = forms.CharField(
731
        label     = app_comment_label,
732
        help_text = app_comment_help,
733
        widget    = forms.Textarea,
734
        required  = False)
735

    
736
    start_date = forms.DateTimeField(
737
        label     = app_start_date_label,
738
        help_text = app_start_date_help,
739
        required  = False)
740

    
741
    end_date = forms.DateTimeField(
742
        label     = app_end_date_label,
743
        help_text = app_end_date_help)
744

    
745
    member_join_policy  = forms.TypedChoiceField(
746
        label     = join_policy_label,
747
        initial   = 2,
748
        coerce    = int,
749
        choices   = join_policies)
750

    
751
    member_leave_policy = forms.TypedChoiceField(
752
        label     = leave_policy_label,
753
        coerce    = int,
754
        choices   = leave_policies)
755

    
756
    limit_on_members_number = forms.IntegerField(
757
        label     = max_members_label,
758
        help_text = max_members_help,
759
        required  = False)
760

    
761
    class Meta:
762
        model = ProjectApplication
763
        fields = ( 'name', 'homepage', 'description',
764
                    'start_date', 'end_date', 'comments',
765
                    'member_join_policy', 'member_leave_policy',
766
                    'limit_on_members_number')
767

    
768
    def __init__(self, *args, **kwargs):
769
        instance = kwargs.get('instance')
770
        self.precursor_application = instance
771
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
772
        # in case of new application remove closed join policy
773
        if not instance:
774
            policies = PROJECT_MEMBER_JOIN_POLICIES.copy()
775
            policies.pop('3')
776
            self.fields['member_join_policy'].choices = policies.iteritems()
777

    
778
    def clean_start_date(self):
779
        start_date = self.cleaned_data.get('start_date')
780
        if not self.precursor_application:
781
            today = datetime.now()
782
            today = datetime(today.year, today.month, today.day)
783
            if start_date and (start_date - today).days < 0:
784
                raise forms.ValidationError(
785
                _(astakos_messages.INVALID_PROJECT_START_DATE))
786
        return start_date
787

    
788
    def clean_end_date(self):
789
        start_date = self.cleaned_data.get('start_date')
790
        end_date = self.cleaned_data.get('end_date')
791
        today = datetime.now()
792
        today = datetime(today.year, today.month, today.day)
793
        if end_date and (end_date - today).days < 0:
794
            raise forms.ValidationError(
795
                _(astakos_messages.INVALID_PROJECT_END_DATE))
796
        if start_date and (end_date - start_date).days <= 0:
797
            raise forms.ValidationError(
798
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
799
        return end_date
800

    
801
    def clean(self):
802
        userid = self.data.get('user', None)
803
        self.user = None
804
        if userid:
805
            try:
806
                self.user = AstakosUser.objects.get(id=userid)
807
            except AstakosUser.DoesNotExist:
808
                pass
809
        if not self.user:
810
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
811
        super(ProjectApplicationForm, self).clean()
812
        return self.cleaned_data
813

    
814
    @property
815
    def resource_policies(self):
816
        policies = []
817
        append = policies.append
818
        for name, value in self.data.iteritems():
819
            if not value:
820
                continue
821
            uplimit = value
822
            if name.endswith('_uplimit'):
823
                subs = name.split('_uplimit')
824
                prefix, suffix = subs
825
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
826
                resource = Resource.objects.get(service__name=s, name=r)
827

    
828
                # keep only resource limits for selected resource groups
829
                if self.data.get(
830
                    'is_selected_%s' % resource.group, "0"
831
                 ) == "1":
832
                    d = model_to_dict(resource)
833
                    if uplimit:
834
                        d.update(dict(service=s, resource=r, uplimit=uplimit))
835
                    else:
836
                        d.update(dict(service=s, resource=r, uplimit=None))
837
                    append(d)
838

    
839
        return policies
840

    
841
    def save(self, commit=True):
842
        data = dict(self.cleaned_data)
843
        data['precursor_application'] = self.instance.id
844
        data['owner'] = self.user
845
        data['resource_policies'] = self.resource_policies
846
        submit_application(data, request_user=self.user)
847

    
848
class ProjectSortForm(forms.Form):
849
    sorting = forms.ChoiceField(
850
        label='Sort by',
851
        choices=(('name', 'Sort by Name'),
852
                 ('issue_date', 'Sort by Issue date'),
853
                 ('start_date', 'Sort by Start Date'),
854
                 ('end_date', 'Sort by End Date'),
855
#                  ('approved_members_num', 'Sort by Participants'),
856
                 ('state', 'Sort by Status'),
857
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
858
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
859
                 ('-name', 'Sort by Name'),
860
                 ('-issue_date', 'Sort by Issue date'),
861
                 ('-start_date', 'Sort by Start Date'),
862
                 ('-end_date', 'Sort by End Date'),
863
#                  ('-approved_members_num', 'Sort by Participants'),
864
                 ('-state', 'Sort by Status'),
865
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
866
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
867
        ),
868
        required=True
869
    )
870

    
871
class AddProjectMembersForm(forms.Form):
872
    q = forms.CharField(
873
        max_length=800, widget=forms.Textarea, label=_('Add members'),
874
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP), required=True)
875

    
876
    def __init__(self, *args, **kwargs):
877
        application_id = kwargs.pop('application_id', None)
878
        if application_id:
879
            self.project = Project.objects.get(application__id=application_id)
880
        self.request_user = kwargs.pop('request_user', None)
881
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
882

    
883
    def clean(self):
884
        try:
885
            do_accept_membership_checks(self.project, self.request_user)
886
        except PermissionDenied, e:
887
            raise forms.ValidationError(e)
888

    
889
        q = self.cleaned_data.get('q') or ''
890
        users = q.split(',')
891
        users = list(u.strip() for u in users if u)
892
        db_entries = AstakosUser.objects.filter(email__in=users)
893
        unknown = list(set(users) - set(u.email for u in db_entries))
894
        if unknown:
895
            raise forms.ValidationError(
896
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
897
        self.valid_users = db_entries
898
        return self.cleaned_data
899

    
900
    def get_valid_users(self):
901
        """Should be called after form cleaning"""
902
        try:
903
            return self.valid_users
904
        except:
905
            return ()
906

    
907
class ProjectMembersSortForm(forms.Form):
908
    sorting = forms.ChoiceField(
909
        label='Sort by',
910
        choices=(('person__email', 'User Id'),
911
                 ('person__first_name', 'Name'),
912
                 ('acceptance_date', 'Acceptance date')
913
        ),
914
        required=True
915
    )
916

    
917

    
918
class ProjectSearchForm(forms.Form):
919
    q = forms.CharField(max_length=200, label='Search project', required=False)
920

    
921

    
922
class ExtendedProfileForm(ProfileForm):
923
    """
924
    Profile form that combines `email change` and `password change` user
925
    actions by propagating submited data to internal EmailChangeForm
926
    and ExtendedPasswordChangeForm objects.
927
    """
928

    
929
    password_change_form = None
930
    email_change_form = None
931

    
932
    password_change = False
933
    email_change = False
934

    
935
    extra_forms_fields = {
936
        'email': ['new_email_address'],
937
        'password': ['old_password', 'new_password1', 'new_password2']
938
    }
939

    
940
    fields = ('email')
941
    change_password = forms.BooleanField(initial=False, required=False)
942
    change_email = forms.BooleanField(initial=False, required=False)
943

    
944
    email_changed = False
945
    password_changed = False
946

    
947
    def __init__(self, *args, **kwargs):
948

    
949
        self.fields_list = [
950
                'email',
951
                'new_email_address',
952
                'first_name',
953
                'last_name',
954
                'auth_token',
955
                'auth_token_expires',
956
                'old_password',
957
                'new_password1',
958
                'new_password2',
959
                'change_email',
960
                'change_password',
961
        ]
962

    
963
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
964
        if self.instance.can_change_password():
965
            self.password_change = True
966
        else:
967
            self.fields_list.remove('old_password')
968
            self.fields_list.remove('new_password1')
969
            self.fields_list.remove('new_password2')
970
            self.fields_list.remove('change_password')
971
            del self.fields['change_password']
972

    
973

    
974
        if EMAILCHANGE_ENABLED and self.instance.can_change_email():
975
            self.email_change = True
976
        else:
977
            self.fields_list.remove('new_email_address')
978
            self.fields_list.remove('change_email')
979
            del self.fields['change_email']
980

    
981

    
982
        self._init_extra_forms()
983
        self.save_extra_forms = []
984
        self.success_messages = []
985
        self.fields.keyOrder = self.fields_list
986

    
987

    
988
    def _init_extra_form_fields(self):
989
        if self.email_change:
990
            self.fields.update(self.email_change_form.fields)
991
            self.fields['new_email_address'].required = False
992

    
993
        if self.password_change:
994
            self.fields.update(self.password_change_form.fields)
995
            self.fields['old_password'].required = False
996
            self.fields['new_password1'].required = False
997
            self.fields['new_password2'].required = False
998

    
999
    def _update_extra_form_errors(self):
1000
        if self.cleaned_data.get('change_password'):
1001
            self.errors.update(self.password_change_form.errors)
1002
        if self.cleaned_data.get('change_email'):
1003
            self.errors.update(self.email_change_form.errors)
1004

    
1005
    def _init_extra_forms(self):
1006
        self.email_change_form = EmailChangeForm(self.data)
1007
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
1008
                                   data=self.data)
1009
        self._init_extra_form_fields()
1010

    
1011
    def is_valid(self):
1012
        password, email = True, True
1013
        profile = super(ExtendedProfileForm, self).is_valid()
1014
        if profile and self.cleaned_data.get('change_password', None):
1015
            password = self.password_change_form.is_valid()
1016
            self.save_extra_forms.append('password')
1017
        if profile and self.cleaned_data.get('change_email'):
1018
            email = self.email_change_form.is_valid()
1019
            self.save_extra_forms.append('email')
1020

    
1021
        if not password or not email:
1022
            self._update_extra_form_errors()
1023

    
1024
        return all([profile, password, email])
1025

    
1026
    def save(self, request, *args, **kwargs):
1027
        if 'email' in self.save_extra_forms:
1028
            self.email_change_form.save(request, *args, **kwargs)
1029
            self.email_changed = True
1030
        if 'password' in self.save_extra_forms:
1031
            self.password_change_form.save(*args, **kwargs)
1032
            self.password_changed = True
1033
        return super(ExtendedProfileForm, self).save(*args, **kwargs)
1034