Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 01223f04

History | View | Annotate | Download (33.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)
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
today = datetime.now()
90
today = datetime(today.year, today.month, today.day)
91

    
92
class StoreUserMixin(object):
93

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

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

    
109

    
110
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
111
    """
112
    Extends the built in UserCreationForm in several ways:
113

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

    
123
    class Meta:
124
        model = AstakosUser
125
        fields = ("email", "first_name", "last_name",
126
                  "has_signed_terms", "has_signed_terms")
127

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

    
137
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
138
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
139
                                'password1', 'password2']
140

    
141
        if RECAPTCHA_ENABLED:
142
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
143
                                         'recaptcha_response_field', ])
144
        if get_latest_terms():
145
            self.fields.keyOrder.append('has_signed_terms')
146

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

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

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

    
169
    def clean_recaptcha_response_field(self):
170
        if 'recaptcha_challenge_field' in self.cleaned_data:
171
            self.validate_captcha()
172
        return self.cleaned_data['recaptcha_response_field']
173

    
174
    def clean_recaptcha_challenge_field(self):
175
        if 'recaptcha_response_field' in self.cleaned_data:
176
            self.validate_captcha()
177
        return self.cleaned_data['recaptcha_challenge_field']
178

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

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

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

    
206

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

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

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

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

    
234

    
235
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
236
    id = forms.CharField(
237
        widget=forms.HiddenInput(),
238
        label='',
239
        required=False
240
    )
241
    third_party_identifier = forms.CharField(
242
        widget=forms.HiddenInput(),
243
        label=''
244
    )
245

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

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

    
258
        latest_terms = get_latest_terms()
259
        if latest_terms:
260
            self._meta.fields.append('has_signed_terms')
261

    
262
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
263

    
264
        if latest_terms:
265
            self.fields.keyOrder.append('has_signed_terms')
266

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

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

    
284
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED) + ' ' + \
285
                                        extra_message)
286
        return email
287

    
288
    def clean_has_signed_terms(self):
289
        has_signed_terms = self.cleaned_data['has_signed_terms']
290
        if not has_signed_terms:
291
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
292
        return has_signed_terms
293

    
294
    def post_store_user(self, user, request):
295
        pending = PendingThirdPartyUser.objects.get(
296
                                token=request.POST.get('third_party_token'),
297
                                third_party_identifier= \
298
                            self.cleaned_data.get('third_party_identifier'))
299
        return user.add_pending_auth_provider(pending)
300

    
301
    def save(self, commit=True):
302
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
303
        user.set_unusable_password()
304
        user.renew_token()
305
        if commit:
306
            user.save()
307
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
308
        return user
309

    
310

    
311
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
312
    """
313
    Extends the ThirdPartyUserCreationForm: email is readonly.
314
    """
315
    def __init__(self, *args, **kwargs):
316
        """
317
        Changes the order of fields, and removes the username field.
318
        """
319
        super(
320
            InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
321

    
322
        #set readonly form fields
323
        ro = ('email',)
324
        for f in ro:
325
            self.fields[f].widget.attrs['readonly'] = True
326

    
327
    def save(self, commit=True):
328
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
329
        user.set_invitation_level()
330
        user.email_verified = True
331
        if commit:
332
            user.save()
333
        return user
334

    
335

    
336
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
337
    additional_email = forms.CharField(
338
        widget=forms.HiddenInput(), label='', required=False)
339

    
340
    def __init__(self, *args, **kwargs):
341
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
342
        # copy email value to additional_mail in case user will change it
343
        name = 'email'
344
        field = self.fields[name]
345
        self.initial['additional_email'] = self.initial.get(name, field.initial)
346
        self.initial['email'] = None
347

    
348

    
349
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
350
                                        InvitedThirdPartyUserCreationForm):
351
    pass
352

    
353

    
354
class LoginForm(AuthenticationForm):
355
    username = forms.EmailField(label=_("Email"))
356
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
357
    recaptcha_response_field = forms.CharField(
358
        widget=RecaptchaWidget, label='')
359

    
360
    def __init__(self, *args, **kwargs):
361
        was_limited = kwargs.get('was_limited', False)
362
        request = kwargs.get('request', None)
363
        if request:
364
            self.ip = request.META.get('REMOTE_ADDR',
365
                                       request.META.get('HTTP_X_REAL_IP', None))
366

    
367
        t = ('request', 'was_limited')
368
        for elem in t:
369
            if elem in kwargs.keys():
370
                kwargs.pop(elem)
371
        super(LoginForm, self).__init__(*args, **kwargs)
372

    
373
        self.fields.keyOrder = ['username', 'password']
374
        if was_limited and RECAPTCHA_ENABLED:
375
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
376
                                         'recaptcha_response_field', ])
377

    
378
    def clean_username(self):
379
        return self.cleaned_data['username'].lower()
380

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

    
386
    def clean_recaptcha_challenge_field(self):
387
        if 'recaptcha_response_field' in self.cleaned_data:
388
            self.validate_captcha()
389
        return self.cleaned_data['recaptcha_challenge_field']
390

    
391
    def validate_captcha(self):
392
        rcf = self.cleaned_data['recaptcha_challenge_field']
393
        rrf = self.cleaned_data['recaptcha_response_field']
394
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
395
        if not check.is_valid:
396
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
397

    
398
    def clean(self):
399
        """
400
        Override default behavior in order to check user's activation later
401
        """
402
        username = self.cleaned_data.get('username')
403

    
404
        if username:
405
            try:
406
                user = AstakosUser.objects.get_by_identifier(username)
407
                if not user.has_auth_provider('local'):
408
                    provider = auth_providers.get_provider('local')
409
                    raise forms.ValidationError(
410
                        _(provider.get_message('NOT_ACTIVE_FOR_USER')))
411
            except AstakosUser.DoesNotExist:
412
                pass
413

    
414
        try:
415
            super(LoginForm, self).clean()
416
        except forms.ValidationError, e:
417
            if self.user_cache is None:
418
                raise
419
            if not self.user_cache.is_active:
420
                raise forms.ValidationError(self.user_cache.get_inactive_message())
421
            if self.request:
422
                if not self.request.session.test_cookie_worked():
423
                    raise
424
        return self.cleaned_data
425

    
426

    
427
class ProfileForm(forms.ModelForm):
428
    """
429
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
430
    Most of the fields are readonly since the user is not allowed to change
431
    them.
432

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

    
438
    class Meta:
439
        model = AstakosUser
440
        fields = ('email', 'first_name', 'last_name', 'auth_token',
441
                  'auth_token_expires')
442

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

    
452
    def save(self, commit=True):
453
        user = super(ProfileForm, self).save(commit=False)
454
        user.is_verified = True
455
        if self.cleaned_data.get('renew'):
456
            user.renew_token(
457
                flush_sessions=True,
458
                current_key=self.session_key
459
            )
460
        if commit:
461
            user.save()
462
        return user
463

    
464

    
465
class FeedbackForm(forms.Form):
466
    """
467
    Form for writing feedback.
468
    """
469
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
470
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
471
                                    required=False)
472

    
473

    
474
class SendInvitationForm(forms.Form):
475
    """
476
    Form for sending an invitations
477
    """
478

    
479
    email = forms.EmailField(required=True, label='Email address')
480
    first_name = forms.EmailField(label='First name')
481
    last_name = forms.EmailField(label='Last name')
482

    
483

    
484
class ExtendedPasswordResetForm(PasswordResetForm):
485
    """
486
    Extends PasswordResetForm by overriding
487

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

    
496
            if not user.is_active:
497
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
498

    
499
            if not user.has_usable_password():
500
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
501

    
502
            if not user.can_change_password():
503
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
504
        except AstakosUser.DoesNotExist, e:
505
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
506
        return email
507

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

    
530

    
531
class EmailChangeForm(forms.ModelForm):
532

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

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

    
543
    def save(self, email_template_name, request, commit=True):
544
        ec = super(EmailChangeForm, self).save(commit=False)
545
        ec.user = request.user
546
        activation_key = hashlib.sha1(
547
            str(random()) + smart_str(ec.new_email_address))
548
        ec.activation_key = activation_key.hexdigest()
549
        if commit:
550
            ec.save()
551
        send_change_email(ec, request, email_template_name=email_template_name)
552

    
553

    
554
class SignApprovalTermsForm(forms.ModelForm):
555

    
556
    class Meta:
557
        model = AstakosUser
558
        fields = ("has_signed_terms",)
559

    
560
    def __init__(self, *args, **kwargs):
561
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
562

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

    
569

    
570
class InvitationForm(forms.ModelForm):
571

    
572
    username = forms.EmailField(label=_("Email"))
573

    
574
    def __init__(self, *args, **kwargs):
575
        super(InvitationForm, self).__init__(*args, **kwargs)
576

    
577
    class Meta:
578
        model = Invitation
579
        fields = ('username', 'realname')
580

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

    
590

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

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

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

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

    
627
    def __init__(self, user, *args, **kwargs):
628
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
629

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

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

    
644

    
645

    
646

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

    
662

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

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

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

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

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

    
700
join_policy_label    =  _("Joining policy")
701
leave_policy_label   =  _("Leaving policy")
702

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

    
712
join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
713
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()
714

    
715
class ProjectApplicationForm(forms.ModelForm):
716

    
717
    name = forms.CharField(
718
        label     = app_name_label,
719
        help_text = app_name_help,
720
        widget    = app_name_widget,
721
        validators = [app_name_validator])
722

    
723
    homepage = forms.URLField(
724
        label     = app_home_label,
725
        help_text = app_home_help,
726
        widget    = app_home_widget,
727
        required  = False)
728

    
729
    description = forms.CharField(
730
        label     = app_desc_label,
731
        help_text = app_desc_help,
732
        widget    = forms.Textarea,
733
        required  = False)
734

    
735
    comments = forms.CharField(
736
        label     = app_comment_label,
737
        help_text = app_comment_help,
738
        widget    = forms.Textarea,
739
        required  = False)
740

    
741
    start_date = forms.DateTimeField(
742
        label     = app_start_date_label,
743
        help_text = app_start_date_help,
744
        required  = False)
745

    
746
    end_date = forms.DateTimeField(
747
        label     = app_end_date_label,
748
        help_text = app_end_date_help)
749

    
750
    member_join_policy  = forms.TypedChoiceField(
751
        label     = join_policy_label,
752
        initial   = 2,
753
        coerce    = int,
754
        choices   = join_policies)
755

    
756
    member_leave_policy = forms.TypedChoiceField(
757
        label     = leave_policy_label,
758
        coerce    = int,
759
        choices   = leave_policies)
760

    
761
    limit_on_members_number = forms.IntegerField(
762
        label     = max_members_label,
763
        help_text = max_members_help,
764
        required  = False)
765

    
766
    class Meta:
767
        model = ProjectApplication
768
        fields = ( 'name', 'homepage', 'description',
769
                    'start_date', 'end_date', 'comments',
770
                    'member_join_policy', 'member_leave_policy',
771
                    'limit_on_members_number')
772

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

    
783
    def clean_start_date(self):
784
        start_date = self.cleaned_data.get('start_date')
785
        if not self.precursor_application:
786
            if start_date and (start_date - today).days < 0:
787
                raise forms.ValidationError(
788
                _(astakos_messages.INVALID_PROJECT_START_DATE))
789
        return start_date
790

    
791
    def clean_end_date(self):
792
        start_date = self.cleaned_data.get('start_date')
793
        end_date = self.cleaned_data.get('end_date')
794
        if end_date and (end_date - today).days < 0:
795
            raise forms.ValidationError(
796
                _(astakos_messages.INVALID_PROJECT_END_DATE))
797
        if start_date and (end_date - start_date).days <= 0:
798
            raise forms.ValidationError(
799
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
800
        return end_date
801

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

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

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

    
840
        return policies
841

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

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

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

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

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

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

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

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

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