Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 6556e514

History | View | Annotate | Download (28 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

    
36
from django import forms
37
from django.utils.translation import ugettext as _
38
from django.contrib.auth.forms import (
39
    UserCreationForm, AuthenticationForm,
40
    PasswordResetForm, PasswordChangeForm,
41
    SetPasswordForm)
42
from django.core.mail import send_mail
43
from django.contrib.auth.tokens import default_token_generator
44
from django.template import Context, loader
45
from django.utils.http import int_to_base36
46
from django.core.urlresolvers import reverse
47
from django.utils.safestring import mark_safe
48
from django.utils.encoding import smart_str
49
from django.conf import settings
50
from django.forms.models import fields_for_model
51
from django.db import transaction
52
from django.utils.encoding import smart_unicode
53
from django.core import validators
54
from django.contrib.auth.models import AnonymousUser
55

    
56
from astakos.im.models import (
57
    AstakosUser, EmailChange, Invitation,
58
    Resource, PendingThirdPartyUser, get_latest_terms, RESOURCE_SEPARATOR,
59
    ProjectApplication)
60
from astakos.im.settings import (
61
    INVITATIONS_PER_LEVEL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY,
62
    RECAPTCHA_ENABLED, DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
63
    PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN,
64
    MODERATION_ENABLED)
65
from astakos.im.widgets import DummyWidget, RecaptchaWidget
66
from astakos.im.functions import send_change_email, submit_application
67

    
68
from astakos.im.util import reserved_email, get_query, model_to_dict
69
from astakos.im import auth_providers
70

    
71
import astakos.im.messages as astakos_messages
72

    
73
import logging
74
import hashlib
75
import recaptcha.client.captcha as captcha
76
import re
77

    
78
logger = logging.getLogger(__name__)
79

    
80
DOMAIN_VALUE_REGEX = re.compile(
81
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
82
    re.IGNORECASE)
83

    
84
class StoreUserMixin(object):
85

    
86
    @transaction.commit_on_success
87
    def store_user(self, user, request):
88
        user.save()
89
        self.post_store_user(user, request)
90
        return user
91

    
92
    def post_store_user(self, user, request):
93
        """
94
        Interface method for descendant backends to be able to do stuff within
95
        the transaction enabled by store_user.
96
        """
97
        pass
98

    
99

    
100
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
101
    """
102
    Extends the built in UserCreationForm in several ways:
103

104
    * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
105
    * The username field isn't visible and it is assigned a generated id.
106
    * User created is not active.
107
    """
108
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
109
    recaptcha_response_field = forms.CharField(
110
        widget=RecaptchaWidget, label='')
111

    
112
    class Meta:
113
        model = AstakosUser
114
        fields = ("email", "first_name", "last_name",
115
                  "has_signed_terms", "has_signed_terms")
116

    
117
    def __init__(self, *args, **kwargs):
118
        """
119
        Changes the order of fields, and removes the username field.
120
        """
121
        request = kwargs.pop('request', None)
122
        if request:
123
            self.ip = request.META.get('REMOTE_ADDR',
124
                                       request.META.get('HTTP_X_REAL_IP', None))
125

    
126
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
127
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
128
                                'password1', 'password2']
129

    
130
        if RECAPTCHA_ENABLED:
131
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
132
                                         'recaptcha_response_field', ])
133
        if get_latest_terms():
134
            self.fields.keyOrder.append('has_signed_terms')
135

    
136
        if 'has_signed_terms' in self.fields:
137
            # Overriding field label since we need to apply a link
138
            # to the terms within the label
139
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
140
                % (reverse('latest_terms'), _("the terms"))
141
            self.fields['has_signed_terms'].label = \
142
                mark_safe("I agree with %s" % terms_link_html)
143

    
144
    def clean_email(self):
145
        email = self.cleaned_data['email']
146
        if not email:
147
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
148
        if reserved_email(email):
149
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
150
        return email
151

    
152
    def clean_has_signed_terms(self):
153
        has_signed_terms = self.cleaned_data['has_signed_terms']
154
        if not has_signed_terms:
155
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
156
        return has_signed_terms
157

    
158
    def clean_recaptcha_response_field(self):
159
        if 'recaptcha_challenge_field' in self.cleaned_data:
160
            self.validate_captcha()
161
        return self.cleaned_data['recaptcha_response_field']
162

    
163
    def clean_recaptcha_challenge_field(self):
164
        if 'recaptcha_response_field' in self.cleaned_data:
165
            self.validate_captcha()
166
        return self.cleaned_data['recaptcha_challenge_field']
167

    
168
    def validate_captcha(self):
169
        rcf = self.cleaned_data['recaptcha_challenge_field']
170
        rrf = self.cleaned_data['recaptcha_response_field']
171
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
172
        if not check.is_valid:
173
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
174

    
175
    def post_store_user(self, user, request):
176
        """
177
        Interface method for descendant backends to be able to do stuff within
178
        the transaction enabled by store_user.
179
        """
180
        user.add_auth_provider('local', auth_backend='astakos')
181
        user.set_password(self.cleaned_data['password1'])
182

    
183
    def save(self, commit=True):
184
        """
185
        Saves the email, first_name and last_name properties, after the normal
186
        save behavior is complete.
187
        """
188
        user = super(LocalUserCreationForm, self).save(commit=False)
189
        user.renew_token()
190
        if commit:
191
            user.save()
192
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
193
        return user
194

    
195

    
196
class InvitedLocalUserCreationForm(LocalUserCreationForm):
197
    """
198
    Extends the LocalUserCreationForm: email is readonly.
199
    """
200
    class Meta:
201
        model = AstakosUser
202
        fields = ("email", "first_name", "last_name", "has_signed_terms")
203

    
204
    def __init__(self, *args, **kwargs):
205
        """
206
        Changes the order of fields, and removes the username field.
207
        """
208
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
209

    
210
        #set readonly form fields
211
        ro = ('email', 'username',)
212
        for f in ro:
213
            self.fields[f].widget.attrs['readonly'] = True
214

    
215
    def save(self, commit=True):
216
        user = super(InvitedLocalUserCreationForm, self).save(commit=False)
217
        user.set_invitations_level()
218
        user.email_verified = True
219
        if commit:
220
            user.save()
221
        return user
222

    
223

    
224
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
225
    id = forms.CharField(
226
        widget=forms.HiddenInput(),
227
        label='',
228
        required=False
229
    )
230
    third_party_identifier = forms.CharField(
231
        widget=forms.HiddenInput(),
232
        label=''
233
    )
234
    class Meta:
235
        model = AstakosUser
236
        fields = ['id', 'email', 'third_party_identifier', 'first_name', 'last_name']
237

    
238
    def __init__(self, *args, **kwargs):
239
        """
240
        Changes the order of fields, and removes the username field.
241
        """
242
        self.request = kwargs.get('request', None)
243
        if self.request:
244
            kwargs.pop('request')
245

    
246
        latest_terms = get_latest_terms()
247
        if latest_terms:
248
            self._meta.fields.append('has_signed_terms')
249

    
250
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
251

    
252
        if latest_terms:
253
            self.fields.keyOrder.append('has_signed_terms')
254

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

    
263
    def clean_email(self):
264
        email = self.cleaned_data['email']
265
        if not email:
266
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
267
        if reserved_email(email):
268
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
269
        return email
270

    
271
    def clean_has_signed_terms(self):
272
        has_signed_terms = self.cleaned_data['has_signed_terms']
273
        if not has_signed_terms:
274
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
275
        return has_signed_terms
276

    
277
    def post_store_user(self, user, request):
278
        pending = PendingThirdPartyUser.objects.get(
279
                                token=request.POST.get('third_party_token'),
280
                                third_party_identifier= \
281
            self.cleaned_data.get('third_party_identifier'))
282
        return user.add_pending_auth_provider(pending)
283

    
284

    
285
    def save(self, commit=True):
286
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
287
        user.set_unusable_password()
288
        user.renew_token()
289
        if commit:
290
            user.save()
291
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
292
        return user
293

    
294

    
295
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
296
    """
297
    Extends the ThirdPartyUserCreationForm: email is readonly.
298
    """
299
    def __init__(self, *args, **kwargs):
300
        """
301
        Changes the order of fields, and removes the username field.
302
        """
303
        super(
304
            InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
305

    
306
        #set readonly form fields
307
        ro = ('email',)
308
        for f in ro:
309
            self.fields[f].widget.attrs['readonly'] = True
310

    
311
    def save(self, commit=True):
312
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
313
        user.set_invitation_level()
314
        user.email_verified = True
315
        if commit:
316
            user.save()
317
        return user
318

    
319

    
320
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
321
    additional_email = forms.CharField(
322
        widget=forms.HiddenInput(), label='', required=False)
323

    
324
    def __init__(self, *args, **kwargs):
325
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
326
        # copy email value to additional_mail in case user will change it
327
        name = 'email'
328
        field = self.fields[name]
329
        self.initial['additional_email'] = self.initial.get(name, field.initial)
330
        self.initial['email'] = None
331

    
332

    
333
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
334
                                        InvitedThirdPartyUserCreationForm):
335
    pass
336

    
337

    
338
class LoginForm(AuthenticationForm):
339
    username = forms.EmailField(label=_("Email"))
340
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
341
    recaptcha_response_field = forms.CharField(
342
        widget=RecaptchaWidget, label='')
343

    
344
    def __init__(self, *args, **kwargs):
345
        was_limited = kwargs.get('was_limited', False)
346
        request = kwargs.get('request', None)
347
        if request:
348
            self.ip = request.META.get('REMOTE_ADDR',
349
                                       request.META.get('HTTP_X_REAL_IP', None))
350

    
351
        t = ('request', 'was_limited')
352
        for elem in t:
353
            if elem in kwargs.keys():
354
                kwargs.pop(elem)
355
        super(LoginForm, self).__init__(*args, **kwargs)
356

    
357
        self.fields.keyOrder = ['username', 'password']
358
        if was_limited and RECAPTCHA_ENABLED:
359
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
360
                                         'recaptcha_response_field', ])
361

    
362
    def clean_username(self):
363
        return self.cleaned_data['username'].lower()
364

    
365
    def clean_recaptcha_response_field(self):
366
        if 'recaptcha_challenge_field' in self.cleaned_data:
367
            self.validate_captcha()
368
        return self.cleaned_data['recaptcha_response_field']
369

    
370
    def clean_recaptcha_challenge_field(self):
371
        if 'recaptcha_response_field' in self.cleaned_data:
372
            self.validate_captcha()
373
        return self.cleaned_data['recaptcha_challenge_field']
374

    
375
    def validate_captcha(self):
376
        rcf = self.cleaned_data['recaptcha_challenge_field']
377
        rrf = self.cleaned_data['recaptcha_response_field']
378
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
379
        if not check.is_valid:
380
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
381

    
382
    def clean(self):
383
        """
384
        Override default behavior in order to check user's activation later
385
        """
386
        username = self.cleaned_data.get('username')
387

    
388
        if username:
389
            try:
390
                user = AstakosUser.objects.get_by_identifier(username)
391
                if not user.has_auth_provider('local'):
392
                    provider = auth_providers.get_provider('local')
393
                    raise forms.ValidationError(
394
                        _(provider.get_message('NOT_ACTIVE_FOR_USER')))
395
            except AstakosUser.DoesNotExist:
396
                pass
397

    
398
        try:
399
            super(LoginForm, self).clean()
400
        except forms.ValidationError, e:
401
            if self.user_cache is None:
402
                raise
403
            if not self.user_cache.is_active:
404
                raise forms.ValidationError(self.user_cache.get_inactive_message())
405
            if self.request:
406
                if not self.request.session.test_cookie_worked():
407
                    raise
408
        return self.cleaned_data
409

    
410

    
411
class ProfileForm(forms.ModelForm):
412
    """
413
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
414
    Most of the fields are readonly since the user is not allowed to change
415
    them.
416

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

    
422
    class Meta:
423
        model = AstakosUser
424
        fields = ('email', 'first_name', 'last_name', 'auth_token',
425
                  'auth_token_expires')
426

    
427
    def __init__(self, *args, **kwargs):
428
        self.session_key = kwargs.pop('session_key', None)
429
        super(ProfileForm, self).__init__(*args, **kwargs)
430
        instance = getattr(self, 'instance', None)
431
        ro_fields = ('email', 'auth_token', 'auth_token_expires')
432
        if instance and instance.id:
433
            for field in ro_fields:
434
                self.fields[field].widget.attrs['readonly'] = True
435

    
436
    def save(self, commit=True):
437
        user = super(ProfileForm, self).save(commit=False)
438
        user.is_verified = True
439
        if self.cleaned_data.get('renew'):
440
            user.renew_token(
441
                flush_sessions=True,
442
                current_key=self.session_key
443
            )
444
        if commit:
445
            user.save()
446
        return user
447

    
448

    
449
class FeedbackForm(forms.Form):
450
    """
451
    Form for writing feedback.
452
    """
453
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
454
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
455
                                    required=False)
456

    
457

    
458
class SendInvitationForm(forms.Form):
459
    """
460
    Form for sending an invitations
461
    """
462

    
463
    email = forms.EmailField(required=True, label='Email address')
464
    first_name = forms.EmailField(label='First name')
465
    last_name = forms.EmailField(label='Last name')
466

    
467

    
468
class ExtendedPasswordResetForm(PasswordResetForm):
469
    """
470
    Extends PasswordResetForm by overriding
471

472
    save method: to pass a custom from_email in send_mail.
473
    clean_email: to handle local auth provider checks
474
    """
475
    def clean_email(self):
476
        email = super(ExtendedPasswordResetForm, self).clean_email()
477
        try:
478
            user = AstakosUser.objects.get_by_identifier(email)
479

    
480
            if not user.is_active:
481
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
482

    
483
            if not user.has_usable_password():
484
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
485

    
486
            if not user.can_change_password():
487
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
488
        except AstakosUser.DoesNotExist, e:
489
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
490
        return email
491

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

    
514

    
515
class EmailChangeForm(forms.ModelForm):
516

    
517
    class Meta:
518
        model = EmailChange
519
        fields = ('new_email_address',)
520

    
521
    def clean_new_email_address(self):
522
        addr = self.cleaned_data['new_email_address']
523
        if AstakosUser.objects.filter(email__iexact=addr):
524
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
525
        return addr
526

    
527
    def save(self, email_template_name, request, commit=True):
528
        ec = super(EmailChangeForm, self).save(commit=False)
529
        ec.user = request.user
530
        activation_key = hashlib.sha1(
531
            str(random()) + smart_str(ec.new_email_address))
532
        ec.activation_key = activation_key.hexdigest()
533
        if commit:
534
            ec.save()
535
        send_change_email(ec, request, email_template_name=email_template_name)
536

    
537

    
538
class SignApprovalTermsForm(forms.ModelForm):
539

    
540
    class Meta:
541
        model = AstakosUser
542
        fields = ("has_signed_terms",)
543

    
544
    def __init__(self, *args, **kwargs):
545
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
546

    
547
    def clean_has_signed_terms(self):
548
        has_signed_terms = self.cleaned_data['has_signed_terms']
549
        if not has_signed_terms:
550
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
551
        return has_signed_terms
552

    
553

    
554
class InvitationForm(forms.ModelForm):
555

    
556
    username = forms.EmailField(label=_("Email"))
557

    
558
    def __init__(self, *args, **kwargs):
559
        super(InvitationForm, self).__init__(*args, **kwargs)
560

    
561
    class Meta:
562
        model = Invitation
563
        fields = ('username', 'realname')
564

    
565
    def clean_username(self):
566
        username = self.cleaned_data['username']
567
        try:
568
            Invitation.objects.get(username=username)
569
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
570
        except Invitation.DoesNotExist:
571
            pass
572
        return username
573

    
574

    
575
class ExtendedPasswordChangeForm(PasswordChangeForm):
576
    """
577
    Extends PasswordChangeForm by enabling user
578
    to optionally renew also the token.
579
    """
580
    if not NEWPASSWD_INVALIDATE_TOKEN:
581
        renew = forms.BooleanField(label='Renew token', required=False,
582
                                   initial=True,
583
                                   help_text='Unsetting this may result in security risk.')
584

    
585
    def __init__(self, user, *args, **kwargs):
586
        self.session_key = kwargs.pop('session_key', None)
587
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
588

    
589
    def save(self, commit=True):
590
        try:
591
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
592
                self.user.renew_token()
593
            self.user.flush_sessions(current_key=self.session_key)
594
        except AttributeError:
595
            # if user model does has not such methods
596
            pass
597
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
598

    
599
class ExtendedSetPasswordForm(SetPasswordForm):
600
    """
601
    Extends SetPasswordForm by enabling user
602
    to optionally renew also the token.
603
    """
604
    if not NEWPASSWD_INVALIDATE_TOKEN:
605
        renew = forms.BooleanField(
606
            label='Renew token',
607
            required=False,
608
            initial=True,
609
            help_text='Unsetting this may result in security risk.')
610

    
611
    def __init__(self, user, *args, **kwargs):
612
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
613

    
614
    @transaction.commit_on_success()
615
    def save(self, commit=True):
616
        try:
617
            self.user = AstakosUser.objects.get(id=self.user.id)
618
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
619
                self.user.renew_token()
620
            #self.user.flush_sessions()
621
            if not self.user.has_auth_provider('local'):
622
                self.user.add_auth_provider('local', auth_backend='astakos')
623

    
624
        except BaseException, e:
625
            logger.exception(e)
626
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
627

    
628

    
629
class ProjectApplicationForm(forms.ModelForm):
630
    name = forms.CharField(
631
        validators=[validators.RegexValidator(
632
            DOMAIN_VALUE_REGEX,
633
            _(astakos_messages.DOMAIN_VALUE_ERR),
634
            'invalid'
635
        )],
636
        widget=forms.TextInput(attrs={'placeholder': 'myproject.mylab.ntua.gr'}),
637
        help_text=" The Project's name should be in a domain format. The domain shouldn't neccessarily exist in the real world but is helpful to imply a structure. e.g.: myproject.mylab.ntua.gr or myservice.myteam.myorganization "
638
    )
639
    homepage = forms.URLField(
640
        label="Homepage Url",
641
        help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",
642
        widget=forms.TextInput(attrs={'placeholder': 'http://myproject.com'}),
643

    
644
        required=False
645
     )
646
    comments = forms.CharField(widget=forms.Textarea, required=False)
647

    
648
    class Meta:
649
        model = ProjectApplication
650
        exclude = (
651
            'project',
652
            'resource_grants', 'id', 'applicant', 'owner',
653
            'precursor_application', 'state', 'issue_date')
654

    
655
    def __init__(self, *args, **kwargs):
656
        self.precursor_application = kwargs.get('instance')
657
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
658

    
659
    def clean(self):
660
        userid = self.data.get('user', None)
661
        self.user = None
662
        if userid:
663
            try:
664
                self.user = AstakosUser.objects.get(id=userid)
665
            except AstakosUser.DoesNotExist:
666
                pass
667
        if not self.user:
668
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
669
        super(ProjectApplicationForm, self).clean()
670
        return self.cleaned_data
671

    
672
    @property
673
    def resource_policies(self):
674
        policies = []
675
        append = policies.append
676
        for name, value in self.data.iteritems():
677
            if not value:
678
                continue
679
            uplimit = value
680
            if name.endswith('_uplimit'):
681
                subs = name.split('_uplimit')
682
                prefix, suffix = subs
683
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
684
                resource = Resource.objects.get(service__name=s, name=r)
685

    
686
                # keep only resource limits for selected resource groups
687
                if self.data.get(
688
                    'is_selected_%s' % resource.group, "0"
689
                 ) == "1":
690
                    d = model_to_dict(resource)
691
                    if uplimit:
692
                        d.update(dict(service=s, resource=r, uplimit=uplimit))
693
                    else:
694
                        d.update(dict(service=s, resource=r, uplimit=None))
695
                    append(d)
696

    
697
        return policies
698

    
699

    
700
    def save(self, commit=True):
701
        application = super(ProjectApplicationForm, self).save(commit=False)
702
        applicant = self.user
703
        comments = self.cleaned_data.pop('comments', None)
704
        return submit_application(
705
            application,
706
            self.resource_policies,
707
            applicant,
708
            comments,
709
            self.precursor_application
710
        )
711

    
712
class ProjectSortForm(forms.Form):
713
    sorting = forms.ChoiceField(
714
        label='Sort by',
715
        choices=(('name', 'Sort by Name'),
716
                 ('issue_date', 'Sort by Issue date'),
717
                 ('start_date', 'Sort by Start Date'),
718
                 ('end_date', 'Sort by End Date'),
719
#                  ('approved_members_num', 'Sort by Participants'),
720
                 ('state', 'Sort by Status'),
721
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
722
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
723
                 ('-name', 'Sort by Name'),
724
                 ('-issue_date', 'Sort by Issue date'),
725
                 ('-start_date', 'Sort by Start Date'),
726
                 ('-end_date', 'Sort by End Date'),
727
#                  ('-approved_members_num', 'Sort by Participants'),
728
                 ('-state', 'Sort by Status'),
729
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
730
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
731
        ),
732
        required=True
733
    )
734

    
735
class AddProjectMembersForm(forms.Form):
736
    q = forms.CharField(
737
        max_length=800, widget=forms.Textarea, label=_('Add members'),
738
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
739
        required=True)
740

    
741
    def clean(self):
742
        q = self.cleaned_data.get('q') or ''
743
        users = q.split(',')
744
        users = list(u.strip() for u in users if u)
745
        db_entries = AstakosUser.objects.filter(email__in=users)
746
        unknown = list(set(users) - set(u.email for u in db_entries))
747
        if unknown:
748
            raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
749
        self.valid_users = db_entries
750
        return self.cleaned_data
751

    
752
    def get_valid_users(self):
753
        """Should be called after form cleaning"""
754
        try:
755
            return self.valid_users
756
        except:
757
            return ()
758

    
759
class ProjectMembersSortForm(forms.Form):
760
    sorting = forms.ChoiceField(
761
        label='Sort by',
762
        choices=(('person__email', 'User Id'),
763
                 ('person__first_name', 'Name'),
764
                 ('acceptance_date', 'Acceptance date')
765
        ),
766
        required=True
767
    )
768

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