Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (36.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
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)
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, email_template_name, request, commit=True):
536
        ec = super(EmailChangeForm, self).save(commit=False)
537
        ec.user = request.user
538
        activation_key = hashlib.sha1(
539
            str(random()) + smart_str(ec.new_email_address))
540
        ec.activation_key = activation_key.hexdigest()
541
        if commit:
542
            ec.save()
543
        send_change_email(ec, request, email_template_name=email_template_name)
544

    
545

    
546
class SignApprovalTermsForm(forms.ModelForm):
547

    
548
    class Meta:
549
        model = AstakosUser
550
        fields = ("has_signed_terms",)
551

    
552
    def __init__(self, *args, **kwargs):
553
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
554

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

    
561

    
562
class InvitationForm(forms.ModelForm):
563

    
564
    username = forms.EmailField(label=_("Email"))
565

    
566
    def __init__(self, *args, **kwargs):
567
        super(InvitationForm, self).__init__(*args, **kwargs)
568

    
569
    class Meta:
570
        model = Invitation
571
        fields = ('username', 'realname')
572

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

    
582

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

    
593
    def __init__(self, user, *args, **kwargs):
594
        self.session_key = kwargs.pop('session_key', None)
595
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
596

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

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

    
619
    def __init__(self, user, *args, **kwargs):
620
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
621

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

    
632
        except BaseException, e:
633
            logger.exception(e)
634
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
635

    
636

    
637

    
638

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

    
654

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

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

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

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

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

    
692
join_policy_label    =  _("Joining policy")
693
leave_policy_label   =  _("Leaving policy")
694

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

    
704
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
705
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
706

    
707
class ProjectApplicationForm(forms.ModelForm):
708

    
709
    name = forms.CharField(
710
        label     = app_name_label,
711
        help_text = app_name_help,
712
        widget    = app_name_widget,
713
        validators = [app_name_validator])
714

    
715
    homepage = forms.URLField(
716
        label     = app_home_label,
717
        help_text = app_home_help,
718
        widget    = app_home_widget,
719
        required  = False)
720

    
721
    description = forms.CharField(
722
        label     = app_desc_label,
723
        help_text = app_desc_help,
724
        widget    = forms.Textarea,
725
        required  = False)
726

    
727
    comments = forms.CharField(
728
        label     = app_comment_label,
729
        help_text = app_comment_help,
730
        widget    = forms.Textarea,
731
        required  = False)
732

    
733
    start_date = forms.DateTimeField(
734
        label     = app_start_date_label,
735
        help_text = app_start_date_help,
736
        required  = False)
737

    
738
    end_date = forms.DateTimeField(
739
        label     = app_end_date_label,
740
        help_text = app_end_date_help)
741

    
742
    member_join_policy  = forms.TypedChoiceField(
743
        label     = join_policy_label,
744
        initial   = 2,
745
        coerce    = int,
746
        choices   = join_policies)
747

    
748
    member_leave_policy = forms.TypedChoiceField(
749
        label     = leave_policy_label,
750
        coerce    = int,
751
        choices   = leave_policies)
752

    
753
    limit_on_members_number = forms.IntegerField(
754
        label     = max_members_label,
755
        help_text = max_members_help,
756
        required  = False)
757

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

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

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

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

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

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

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

    
836
        return policies
837

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

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

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

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

    
880
    def clean(self):
881
        try:
882
            do_accept_membership_checks(self.project, self.request_user)
883
        except PermissionDenied, e:
884
            raise forms.ValidationError(e)
885

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

    
897
    def get_valid_users(self):
898
        """Should be called after form cleaning"""
899
        try:
900
            return self.valid_users
901
        except:
902
            return ()
903

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

    
914

    
915
class ProjectSearchForm(forms.Form):
916
    q = forms.CharField(max_length=200, label='Search project', required=False)
917

    
918

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

    
926
    password_change_form = None
927
    email_change_form = None
928
    extra_forms_fields = {
929
        'email': ['new_email_address'],
930
        'password': ['old_password', 'new_password1', 'new_password2']
931
    }
932

    
933
    change_password = forms.BooleanField(initial=False, required=False)
934
    change_email = forms.BooleanField(initial=False, required=False)
935

    
936
    def __init__(self, *args, **kwargs):
937
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
938
        self._init_extra_forms()
939
        self.save_extra_forms = []
940
        self.success_messages = []
941

    
942
    def _init_extra_form_fields(self):
943
        self.fields.update(self.email_change_form.fields)
944
        self.fields.update(self.password_change_form.fields)
945

    
946
        self.fields['new_email_address'].required = False
947
        self.fields['old_password'].required = False
948
        self.fields['new_password1'].required = False
949
        self.fields['new_password2'].required = False
950

    
951
    def _update_extra_form_errors(self):
952
        if self.cleaned_data.get('change_password'):
953
            self.errors.update(self.password_change_form.errors)
954
        if self.cleaned_data.get('change_email'):
955
            self.errors.update(self.email_change_form.errors)
956

    
957
    def _init_extra_forms(self):
958
        self.email_change_form = EmailChangeForm(self.data)
959
        self.password_change_form = ExtendedPasswordChangeForm(user=self.instance,
960
                                   data=self.data)
961
        self._init_extra_form_fields()
962

    
963
    def is_valid(self):
964
        password, email = True, True
965
        profile = super(ExtendedProfileForm, self).is_valid()
966
        if profile and self.cleaned_data.get('change_password'):
967
            password = self.password_change_form.is_valid()
968
        if profile and self.cleaned_data.get('change_email'):
969
            email = self.email_change_form.is_valid()
970

    
971
        if not password or not email:
972
            self._update_extra_form_errors()
973

    
974
        return all([profile, password, email])
975

    
976
    def save(self, *args, **kwargs):
977
        if 'email' in self.save_extra_forms:
978
            self.email_change_email.save(*args, **kwargs)
979
        if 'password' in self.save_extra_forms:
980
            self.password_change_form.save(*args, **kwargs)
981
        return super(ExtendedProfileForm, self).save(*args, **kwargs)
982