Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 34a76cdb

History | View | Annotate | Download (32.8 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
        try:
389
            user = AstakosUser.objects.get_by_identifier(username)
390
            if not user.has_auth_provider('local'):
391
                provider = auth_providers.get_provider('local')
392
                raise forms.ValidationError(
393
                    _(provider.get_message('NOT_ACTIVE_FOR_USER_LOGIN')))
394
        except AstakosUser.DoesNotExist:
395
            pass
396

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

    
409

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

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

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

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

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

    
447

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

    
456

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

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

    
466

    
467
class ExtendedPasswordResetForm(PasswordResetForm):
468
    """
469
    Extends PasswordResetForm by overriding save method:
470
    passes a custom from_email in send_mail.
471

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

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

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

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

    
512

    
513
class EmailChangeForm(forms.ModelForm):
514

    
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

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

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

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

    
551

    
552
class InvitationForm(forms.ModelForm):
553

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

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

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

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

    
572

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

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

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

    
597

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

    
643
    class Meta:
644
        model = AstakosGroup
645

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

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

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

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

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

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

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

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

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

    
719
    class Meta:
720
        model = AstakosGroup
721

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

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

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

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

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

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

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

    
785

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

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

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

    
810

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

    
814

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

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

    
846

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

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

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

    
877

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

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

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

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