Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 67be1883

History | View | Annotate | Download (32.9 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 (UserCreationForm, AuthenticationForm,
39
                                       PasswordResetForm, PasswordChangeForm,
40
                                       SetPasswordForm)
41
from django.core.mail import send_mail
42
from django.contrib.auth.tokens import default_token_generator
43
from django.template import Context, loader
44
from django.utils.http import int_to_base36
45
from django.core.urlresolvers import reverse
46
from django.utils.safestring import mark_safe
47
from django.utils.encoding import smart_str
48
from django.conf import settings
49
from django.forms.models import fields_for_model
50
from django.db import transaction
51
from django.utils.encoding import smart_unicode
52
from django.core import validators
53

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

    
67
from astakos.im.util import reserved_email, get_query
68

    
69
import astakos.im.messages as astakos_messages
70

    
71
import logging
72
import hashlib
73
import recaptcha.client.captcha as captcha
74
import re
75

    
76
logger = logging.getLogger(__name__)
77

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

    
83
class StoreUserMixin(object):
84
    @transaction.commit_on_success
85
    def store_user(self, user, request):
86
        user.save()
87
        self.post_store_user(user, request)
88
        return user
89

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

    
97

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
193

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

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

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

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

    
221

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

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

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

    
248
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
249

    
250
        if latest_terms:
251
            self.fields.keyOrder.append('has_signed_terms')
252

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

    
261
    def clean_email(self):
262
        email = self.cleaned_data['email'].lower()
263
        if not email:
264
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
265
        return email
266

    
267
    def clean_has_signed_terms(self):
268
        has_signed_terms = self.cleaned_data['has_signed_terms']
269
        if not has_signed_terms:
270
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
271
        return has_signed_terms
272

    
273
    def post_store_user(self, user, request):
274
        pending = PendingThirdPartyUser.objects.get(
275
                                token=request.POST.get('third_party_token'),
276
                                third_party_identifier= \
277
            self.cleaned_data.get('third_party_identifier'))
278
        return user.add_pending_auth_provider(pending)
279

    
280

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

    
290

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

    
302
        #set readonly form fields
303
        ro = ('email',)
304
        for f in ro:
305
            self.fields[f].widget.attrs['readonly'] = True
306

    
307
    def save(self, commit=True):
308
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
309
        user.set_invitation_level()
310
        user.email_verified = True
311
        if commit:
312
            user.save()
313
        return user
314

    
315

    
316
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
317
    additional_email = forms.CharField(
318
        widget=forms.HiddenInput(), label='', required=False)
319

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

    
328
    def clean_email(self):
329
        email = self.cleaned_data['email'].lower()
330
        if self.instance:
331
            if self.instance.email == email:
332
                raise forms.ValidationError(_("This is your current email."))
333
        for user in AstakosUser.objects.filter(email__iexact=email):
334
            if user.provider == 'shibboleth':
335
                raise forms.ValidationError(_(
336
                        "This email is already associated with another \
337
                         shibboleth account."
338
                    )
339
                )
340
            else:
341
                raise forms.ValidationError(_("This email is already used"))
342
        super(ShibbolethUserCreationForm, self).clean_email()
343
        return email
344

    
345

    
346
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
347
                                        InvitedThirdPartyUserCreationForm):
348
    pass
349

    
350

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

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

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

    
370
        self.fields.keyOrder = ['username', 'password']
371
        if was_limited and RECAPTCHA_ENABLED:
372
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
373
                                         'recaptcha_response_field', ])
374

    
375
    def clean_username(self):
376
        if 'username' in self.cleaned_data:
377
            return self.cleaned_data['username'].lower()
378

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

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

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

    
396
    def clean(self):
397
        """
398
        Override default behavior in order to check user's activation later
399
        """
400
        try:
401
            super(LoginForm, self).clean()
402
        except forms.ValidationError, e:
403
#            if self.user_cache is None:
404
#                raise
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 save method:
471
    passes a custom from_email in send_mail.
472

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

    
483
            if not user.can_change_password():
484
                raise forms.ValidationError(_('Password change for this account'
485
                                              ' is not supported.'))
486

    
487
        except AstakosUser.DoesNotExist, e:
488
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
489
        return email
490

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

    
513

    
514
class EmailChangeForm(forms.ModelForm):
515
    class Meta:
516
        model = EmailChange
517
        fields = ('new_email_address',)
518

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

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

    
535

    
536
class SignApprovalTermsForm(forms.ModelForm):
537
    class Meta:
538
        model = AstakosUser
539
        fields = ("has_signed_terms",)
540

    
541
    def __init__(self, *args, **kwargs):
542
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
543

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

    
550

    
551
class InvitationForm(forms.ModelForm):
552
    username = forms.EmailField(label=_("Email"))
553

    
554
    def __init__(self, *args, **kwargs):
555
        super(InvitationForm, self).__init__(*args, **kwargs)
556

    
557
    class Meta:
558
        model = Invitation
559
        fields = ('username', 'realname')
560

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

    
570

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

    
581
    def __init__(self, user, *args, **kwargs):
582
        self.session_key = kwargs.pop('session_key', None)
583
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
584

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

    
595

    
596
class AstakosGroupCreationForm(forms.ModelForm):
597
    kind = forms.ModelChoiceField(
598
        queryset=GroupKind.objects.all(),
599
        label="",
600
        widget=forms.HiddenInput()
601
    )
602
    name = forms.CharField(
603
        validators=[validators.RegexValidator(
604
            DOMAIN_VALUE_REGEX,
605
            _(astakos_messages.DOMAIN_VALUE_ERR), 'invalid'
606
        )],
607
        widget=forms.TextInput(attrs={'placeholder': 'myproject.mylab.ntua.gr'}),
608
        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 "
609
    )
610
    homepage = forms.URLField(
611
        label= 'Homepage Url',
612
        widget=forms.TextInput(attrs={'placeholder': 'http://myproject.com'}),
613
        help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",
614
        required=False
615
    )
616
    desc = forms.CharField(
617
        label= 'Description',
618
        widget=forms.Textarea, 
619
        help_text= "Please provide a short but descriptive abstract of your Project, so that anyone searching can quickly understand what this Project is about. "
620
    )
621
    issue_date = forms.DateTimeField(
622
        label= 'Start date',
623
        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."
624
    )
625
    expiration_date = forms.DateTimeField(
626
        label= 'End date',
627
        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.  "
628
    )
629
    moderation_enabled = forms.BooleanField(
630
        label= 'Moderated',
631
        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. ",
632
        required=False,
633
        initial=True
634
    )
635
    max_participants = forms.IntegerField(
636
        label='Total number of members',
637
        required=True, min_value=1,
638
        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. "
639
    )
640

    
641
    class Meta:
642
        model = AstakosGroup
643

    
644
    def __init__(self, *args, **kwargs):
645
        #update QueryDict
646
        args = list(args)
647
        qd = args.pop(0).copy()
648
        members_unlimited = qd.pop('members_unlimited', False)
649
        members_uplimit = qd.pop('members_uplimit', None)
650

    
651
        #substitue QueryDict
652
        args.insert(0, qd)
653

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

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

    
679
    def policies(self):
680
        self.clean()
681
        policies = []
682
        append = policies.append
683
        for name, uplimit in self.cleaned_data.iteritems():
684

    
685
            subs = name.split('_uplimit')
686
            if len(subs) == 2:
687
                prefix, suffix = subs
688
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
689
                resource = Resource.objects.get(service__name=s, name=r)
690

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

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

    
717
    class Meta:
718
        model = AstakosGroup
719

    
720
    def __init__(self, *args, **kwargs):
721
        #update QueryDict
722
        args = list(args)
723
        qd = args.pop(0).copy()
724
        members_unlimited = qd.pop('members_unlimited', False)
725
        members_uplimit = qd.pop('members_uplimit', None)
726

    
727
        #substitue QueryDict
728
        args.insert(0, qd)
729

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

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

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

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

    
778
class AstakosGroupUpdateForm(forms.ModelForm):
779
    class Meta:
780
        model = AstakosGroup
781
        fields = ( 'desc','homepage', 'moderation_enabled')
782

    
783

    
784
class AddGroupMembersForm(forms.Form):
785
    q = forms.CharField(
786
        max_length=800, widget=forms.Textarea, label=_('Add members'),
787
        help_text=_(astakos_messages.ADD_GROUP_MEMBERS_Q_HELP),
788
        required=True)
789

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

    
801
    def get_valid_users(self):
802
        """Should be called after form cleaning"""
803
        try:
804
            return self.valid_users
805
        except:
806
            return ()
807

    
808

    
809
class AstakosGroupSearchForm(forms.Form):
810
    q = forms.CharField(max_length=200, label='Search project')
811

    
812

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

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

    
844

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

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

    
869
class PickResourceForm(forms.Form):
870
    resource = forms.ModelChoiceField(
871
        queryset=Resource.objects.select_related().all()
872
    )
873
    resource.widget.attrs["onchange"] = "this.form.submit()"
874

    
875

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

    
889
    def __init__(self, user, *args, **kwargs):
890
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
891

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

    
902
        except BaseException, e:
903
            logger.exception(e)
904
        return super(ExtendedSetPasswordForm, self).save(commit=commit)