Statistics
| Branch: | Tag: | Revision:

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

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
from astakos.im import auth_providers
69

    
70
import astakos.im.messages as astakos_messages
71

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

    
77
logger = logging.getLogger(__name__)
78

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

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

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

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

    
536

    
537
class SignApprovalTermsForm(forms.ModelForm):
538

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

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

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

    
552

    
553
class InvitationForm(forms.ModelForm):
554

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

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

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

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

    
573

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

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

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

    
598

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

    
644
    class Meta:
645
        model = AstakosGroup
646

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

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

    
657
        super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
658

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

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

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

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

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

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

    
720
    class Meta:
721
        model = AstakosGroup
722

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

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

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

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

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

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

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

    
786

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

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

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

    
811

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

    
815

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

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

    
847

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

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

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

    
878

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

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

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

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