Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 0cc22d47

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

    
57
from astakos.im.models import (
58
    AstakosUser, EmailChange, AstakosGroup, Invitation, GroupKind,
59
    Resource, PendingThirdPartyUser, get_latest_terms, RESOURCE_SEPARATOR,
60
    ProjectDefinition, ProjectApplication
61
)
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
67
)
68
from astakos.im.widgets import DummyWidget, RecaptchaWidget
69
from astakos.im.functions import send_change_email
70

    
71
from astakos.im.util import reserved_email, get_query
72

    
73
import astakos.im.messages as astakos_messages
74

    
75
import logging
76
import hashlib
77
import recaptcha.client.captcha as captcha
78
import re
79

    
80
logger = logging.getLogger(__name__)
81

    
82
DOMAIN_VALUE_REGEX = re.compile(
83
    r'^(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.){0,126}(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?))$',
84
    re.IGNORECASE
85
)
86

    
87
class StoreUserMixin(object):
88
    @transaction.commit_on_success
89
    def store_user(self, user, request):
90
        user.save()
91
        self.post_store_user(user, request)
92
        return user
93

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

    
101

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
197

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

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

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

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

    
225

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

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

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

    
252
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
253

    
254
        if latest_terms:
255
            self.fields.keyOrder.append('has_signed_terms')
256

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

    
265
    def clean_email(self):
266
        email = self.cleaned_data['email'].lower()
267
        if not email:
268
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
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
    def clean_email(self):
333
        email = self.cleaned_data['email'].lower()
334
        if self.instance:
335
            if self.instance.email == email:
336
                raise forms.ValidationError(_("This is your current email."))
337
        for user in AstakosUser.objects.filter(email__iexact=email):
338
            if user.provider == 'shibboleth':
339
                raise forms.ValidationError(_(
340
                        "This email is already associated with another \
341
                         shibboleth account."
342
                    )
343
                )
344
            else:
345
                raise forms.ValidationError(_("This email is already used"))
346
        super(ShibbolethUserCreationForm, self).clean_email()
347
        return email
348

    
349

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

    
354

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

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

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

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

    
379
    def clean_username(self):
380
        if 'username' in self.cleaned_data:
381
            return self.cleaned_data['username'].lower()
382

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

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

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

    
400
    def clean(self):
401
        """
402
        Override default behavior in order to check user's activation later
403
        """
404
        try:
405
            super(LoginForm, self).clean()
406
        except forms.ValidationError, e:
407
#            if self.user_cache is None:
408
#                raise
409
            if self.request:
410
                if not self.request.session.test_cookie_worked():
411
                    raise
412
        return self.cleaned_data
413

    
414

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

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

    
426
    class Meta:
427
        model = AstakosUser
428
        fields = ('email', 'first_name', 'last_name', 'auth_token',
429
                  'auth_token_expires')
430

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

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

    
452

    
453
class FeedbackForm(forms.Form):
454
    """
455
    Form for writing feedback.
456
    """
457
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
458
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
459
                                    required=False)
460

    
461

    
462
class SendInvitationForm(forms.Form):
463
    """
464
    Form for sending an invitations
465
    """
466

    
467
    email = forms.EmailField(required=True, label='Email address')
468
    first_name = forms.EmailField(label='First name')
469
    last_name = forms.EmailField(label='Last name')
470

    
471

    
472
class ExtendedPasswordResetForm(PasswordResetForm):
473
    """
474
    Extends PasswordResetForm by overriding save method:
475
    passes a custom from_email in send_mail.
476

477
    Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
478
    accepts a from_email argument.
479
    """
480
    def clean_email(self):
481
        email = super(ExtendedPasswordResetForm, self).clean_email()
482
        try:
483
            user = AstakosUser.objects.get(email__iexact=email, is_active=True)
484
            if not user.has_usable_password():
485
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
486

    
487
            if not user.can_change_password():
488
                raise forms.ValidationError(_('Password change for this account'
489
                                              ' is not supported.'))
490

    
491
        except AstakosUser.DoesNotExist, e:
492
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
493
        return email
494

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

    
517

    
518
class EmailChangeForm(forms.ModelForm):
519
    class Meta:
520
        model = EmailChange
521
        fields = ('new_email_address',)
522

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

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

    
539

    
540
class SignApprovalTermsForm(forms.ModelForm):
541
    class Meta:
542
        model = AstakosUser
543
        fields = ("has_signed_terms",)
544

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

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

    
554

    
555
class InvitationForm(forms.ModelForm):
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

    
600
class AstakosGroupCreationForm(forms.ModelForm):
601
    kind = forms.ModelChoiceField(
602
        queryset=GroupKind.objects.all(),
603
        label="",
604
        widget=forms.HiddenInput()
605
    )
606
    name = forms.CharField(
607
        validators=[validators.RegexValidator(
608
            DOMAIN_VALUE_REGEX,
609
            _(astakos_messages.DOMAIN_VALUE_ERR), 'invalid'
610
        )],
611
        widget=forms.TextInput(attrs={'placeholder': 'myproject.mylab.ntua.gr'}),
612
        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 "
613
    )
614
    homepage = forms.URLField(
615
        label= 'Homepage Url',
616
        widget=forms.TextInput(attrs={'placeholder': 'http://myproject.com'}),
617
        help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",
618
        required=False
619
    )
620
    desc = forms.CharField(
621
        label= 'Description',
622
        widget=forms.Textarea, 
623
        help_text= "Please provide a short but descriptive abstract of your Project, so that anyone searching can quickly understand what this Project is about. "
624
    )
625
    issue_date = forms.DateTimeField(
626
        label= 'Start date',
627
        help_text= "Here you specify the date you want your Project to start granting its resources. Its members will get the resources coming from this Project on this exact date."
628
    )
629
    expiration_date = forms.DateTimeField(
630
        label= 'End date',
631
        help_text= "Here you specify the date you want your Project to cease. This means that after this date all members will no longer be able to allocate resources from this Project.  "
632
    )
633
    moderation_enabled = forms.BooleanField(
634
        label= 'Moderated',
635
        help_text="Select this to approve each member manually, before they become a part of your Project (default). Be sure you know what you are doing, if you uncheck this option. ",
636
        required=False,
637
        initial=True
638
    )
639
    max_participants = forms.IntegerField(
640
        label='Total number of members',
641
        required=True, min_value=1,
642
        help_text="Here you specify the number of members this Project is going to have. This means that this number of people will be granted the resources you will specify in the next step. This can be '1' if you are the only one wanting to get resources. "
643
    )
644

    
645
    class Meta:
646
        model = AstakosGroup
647

    
648
    def __init__(self, *args, **kwargs):
649
        #update QueryDict
650
        args = list(args)
651
        qd = args.pop(0).copy()
652
        members_unlimited = qd.pop('members_unlimited', False)
653
        members_uplimit = qd.pop('members_uplimit', None)
654

    
655
        #substitue QueryDict
656
        args.insert(0, qd)
657

    
658
        super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
659
        
660
        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
661
                                'issue_date', 'expiration_date',
662
                                'moderation_enabled', 'max_participants']
663
        def add_fields((k, v)):
664
            k = k.partition('_proxy')[0]
665
            self.fields[k] = forms.IntegerField(
666
                required=False,
667
                widget=forms.HiddenInput(),
668
                min_value=1
669
            )
670
        map(add_fields,
671
            ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
672
        )
673

    
674
        def add_fields((k, v)):
675
            self.fields[k] = forms.BooleanField(
676
                required=False,
677
                #widget=forms.HiddenInput()
678
            )
679
        map(add_fields,
680
            ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
681
        )
682

    
683
    def policies(self):
684
        self.clean()
685
        policies = []
686
        append = policies.append
687
        for name, uplimit in self.cleaned_data.iteritems():
688

    
689
            subs = name.split('_uplimit')
690
            if len(subs) == 2:
691
                prefix, suffix = subs
692
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
693
                resource = Resource.objects.get(service__name=s, name=r)
694

    
695
                # keep only resource limits for selected resource groups
696
                if self.cleaned_data.get(
697
                    'is_selected_%s' % resource.group, False
698
                ):
699
                    append(dict(service=s, resource=r, uplimit=uplimit))
700
        return policies
701

    
702
class AstakosGroupCreationSummaryForm(forms.ModelForm):
703
    kind = forms.ModelChoiceField(
704
        queryset=GroupKind.objects.all(),
705
        label="",
706
        widget=forms.HiddenInput()
707
    )
708
    name = forms.CharField(
709
        widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}),
710
        help_text="Name should be in the form of dns"
711
    )
712
    moderation_enabled = forms.BooleanField(
713
        help_text="Check if you want to approve members participation manually",
714
        required=False,
715
        initial=True
716
    )
717
    max_participants = forms.IntegerField(
718
        required=False, min_value=1
719
    )
720

    
721
    class Meta:
722
        model = AstakosGroup
723

    
724
    def __init__(self, *args, **kwargs):
725
        #update QueryDict
726
        args = list(args)
727
        qd = args.pop(0).copy()
728
        members_unlimited = qd.pop('members_unlimited', False)
729
        members_uplimit = qd.pop('members_uplimit', None)
730

    
731
        #substitue QueryDict
732
        args.insert(0, qd)
733

    
734
        super(AstakosGroupCreationSummaryForm, self).__init__(*args, **kwargs)
735
        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
736
                                'issue_date', 'expiration_date',
737
                                'moderation_enabled', 'max_participants']
738
        def add_fields((k, v)):
739
            self.fields[k] = forms.IntegerField(
740
                required=False,
741
                widget=forms.TextInput(),
742
                min_value=1
743
            )
744
        map(add_fields,
745
            ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
746
        )
747

    
748
        def add_fields((k, v)):
749
            self.fields[k] = forms.BooleanField(
750
                required=False,
751
                widget=forms.HiddenInput()
752
            )
753
        map(add_fields,
754
            ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
755
        )
756
        for f in self.fields.values():
757
            f.widget = forms.HiddenInput()
758

    
759
    def clean(self):
760
        super(AstakosGroupCreationSummaryForm, self).clean()
761
        self.cleaned_data['policies'] = []
762
        append = self.cleaned_data['policies'].append
763
        #tbd = [f for f in self.fields if (f.startswith('is_selected_') and (not f.endswith('_proxy')))]
764
        tbd = [f for f in self.fields if f.startswith('is_selected_')]
765
        for name, uplimit in self.cleaned_data.iteritems():
766
            subs = name.split('_uplimit')
767
            if len(subs) == 2:
768
                tbd.append(name)
769
                prefix, suffix = subs
770
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
771
                resource = Resource.objects.get(service__name=s, name=r)
772

    
773
                # keep only resource limits for selected resource groups
774
                if self.cleaned_data.get(
775
                    'is_selected_%s' % resource.group, False
776
                ):
777
                    append(dict(service=s, resource=r, uplimit=uplimit))
778
        for name in tbd:
779
            self.cleaned_data.pop(name, None)
780
        return self.cleaned_data
781

    
782
class AstakosGroupUpdateForm(forms.ModelForm):
783
    class Meta:
784
        model = AstakosGroup
785
        fields = ( 'desc','homepage', 'moderation_enabled')
786

    
787

    
788
class AddGroupMembersForm(forms.Form):
789
    q = forms.CharField(
790
        max_length=800, widget=forms.Textarea, label=_('Add members'),
791
        help_text=_(astakos_messages.ADD_GROUP_MEMBERS_Q_HELP),
792
        required=True)
793

    
794
    def clean(self):
795
        q = self.cleaned_data.get('q') or ''
796
        users = q.split(',')
797
        users = list(u.strip() for u in users if u)
798
        db_entries = AstakosUser.objects.filter(email__in=users)
799
        unknown = list(set(users) - set(u.email for u in db_entries))
800
        if unknown:
801
            raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
802
        self.valid_users = db_entries
803
        return self.cleaned_data
804

    
805
    def get_valid_users(self):
806
        """Should be called after form cleaning"""
807
        try:
808
            return self.valid_users
809
        except:
810
            return ()
811

    
812

    
813
class AstakosGroupSearchForm(forms.Form):
814
    q = forms.CharField(max_length=200, label='Search project')
815

    
816

    
817
class TimelineForm(forms.Form):
818
    entity = forms.ModelChoiceField(
819
        queryset=AstakosUser.objects.filter(is_active=True)
820
    )
821
    resource = forms.ModelChoiceField(
822
        queryset=Resource.objects.all()
823
    )
824
    start_date = forms.DateTimeField()
825
    end_date = forms.DateTimeField()
826
    details = forms.BooleanField(required=False, label="Detailed Listing")
827
    operation = forms.ChoiceField(
828
        label='Charge Method',
829
        choices=(('', '-------------'),
830
                 ('charge_usage', 'Charge Usage'),
831
                 ('charge_traffic', 'Charge Traffic'), )
832
    )
833

    
834
    def clean(self):
835
        super(TimelineForm, self).clean()
836
        d = self.cleaned_data
837
        if 'resource' in d:
838
            d['resource'] = str(d['resource'])
839
        if 'start_date' in d:
840
            d['start_date'] = d['start_date'].strftime(
841
                "%Y-%m-%dT%H:%M:%S.%f")[:24]
842
        if 'end_date' in d:
843
            d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
844
        if 'entity' in d:
845
            d['entity'] = d['entity'].email
846
        return d
847

    
848

    
849
class AstakosGroupSortForm(forms.Form):
850
    sorting = forms.ChoiceField(
851
        label='Sort by',
852
        choices=(
853
            ('groupname', 'Name'),
854
            ('issue_date', 'Issue Date'),
855
            ('expiration_date', 'Expiration Date'),
856
            ('approved_members_num', 'Participants'),
857
            ('moderation_enabled', 'Moderation'),
858
            ('membership_status', 'Enrollment Status')
859
        ),
860
        required=True
861
    )
862

    
863
class MembersSortForm(forms.Form):
864
    sorting = forms.ChoiceField(
865
        label='Sort by',
866
        choices=(('person__email', 'User Id'),
867
                 ('person__first_name', 'Name'),
868
                 ('date_joined', 'Status')
869
        ),
870
        required=True
871
    )
872

    
873
class PickResourceForm(forms.Form):
874
    resource = forms.ModelChoiceField(
875
        queryset=Resource.objects.select_related().all()
876
    )
877
    resource.widget.attrs["onchange"] = "this.form.submit()"
878

    
879

    
880
class ExtendedSetPasswordForm(SetPasswordForm):
881
    """
882
    Extends SetPasswordForm by enabling user
883
    to optionally renew also the token.
884
    """
885
    if not NEWPASSWD_INVALIDATE_TOKEN:
886
        renew = forms.BooleanField(
887
            label='Renew token',
888
            required=False,
889
            initial=True,
890
            help_text='Unsetting this may result in security risk.'
891
        )
892

    
893
    def __init__(self, user, *args, **kwargs):
894
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
895

    
896
    @transaction.commit_on_success()
897
    def save(self, commit=True):
898
        try:
899
            self.user = AstakosUser.objects.get(id=self.user.id)
900
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
901
                self.user.renew_token()
902
            #self.user.flush_sessions()
903
            if not self.user.has_auth_provider('local'):
904
                self.user.add_auth_provider('local', auth_backend='astakos')
905

    
906
        except BaseException, e:
907
            logger.exception(e)
908
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
909

    
910

    
911
class ProjectApplicationForm(forms.ModelForm):
912
    name = forms.CharField(
913
        validators=[validators.RegexValidator(
914
            DOMAIN_VALUE_REGEX,
915
            _(astakos_messages.DOMAIN_VALUE_ERR),
916
            'invalid'
917
        )],
918
        widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}),
919
        help_text="Name should be in the form of dns"
920
    )
921
    comments = forms.CharField(widget=forms.Textarea, required=False)
922
    
923
    class Meta:
924
        model = ProjectDefinition
925
        exclude = ('resource_grants')
926
    
927
    
928
    def clean(self):
929
        userid = self.data.get('user', None)[0]
930
        self.user = None
931
        if userid:
932
            try:
933
                self.user = AstakosUser.objects.get(id=userid)
934
            except AstakosUser.DoesNotExist:
935
                pass
936
        if not self.user:
937
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
938
        super(ProjectApplicationForm, self).clean()
939
        return self.cleaned_data
940
    
941
    @property
942
    def resource_policies(self):
943
        policies = []
944
        append = policies.append
945
        for name, value in self.data.iteritems():
946
            if not value:
947
                continue
948
            uplimit = value[0]
949
            subs = name.split('_uplimit')
950
            if len(subs) == 2:
951
                prefix, suffix = subs
952
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
953
                resource = Resource.objects.get(service__name=s, name=r)
954

    
955
                # keep only resource limits for selected resource groups
956
#                 if self.data.get(
957
#                     'is_selected_%s' % resource.group, False
958
#                 ):
959
                if uplimit:
960
                    append(dict(service=s, resource=r, uplimit=uplimit))
961
        return policies
962

    
963
    def save(self, commit=True):
964
        definition = super(ProjectApplicationForm, self).save(commit=commit)
965
        definition.resource_policies=self.resource_policies
966
        applicant = self.user
967
        comments = self.cleaned_data.pop('comments', None)
968
        try:
969
            precursor_application = self.instance.projectapplication
970
        except:
971
            precursor_application = None
972
        return ProjectApplication.submit(
973
            definition,
974
            applicant,
975
            comments,
976
            precursor_application,
977
            commit
978
        )